Compare commits

..

No commits in common. "a40d16dae0bb6256f40f2e04b9819b0c64432244" and "905c241daaa777e12c7d76b452e8ed7ddb9c7883" have entirely different histories.

10 changed files with 27 additions and 591 deletions

View File

@ -1,13 +1,12 @@
# WebSocket Relay Server # WebSocket Relay Server
A minimal Go WebSocket relay server that broadcasts every incoming message to all connected clients. Supports TLS, Prometheus metrics, configurable logging, and graceful shutdown. A minimal Go WebSocket relay server that broadcasts every incoming message to all connected clients. Supports TLS, Prometheus metrics, and graceful shutdown.
## Features ## Features
- **Fan-out broadcasting** — every message is relayed to all connected clients - **Fan-out broadcasting** — every message is relayed to all connected clients
- **TLS support** — optional `wss://` via cert/key PEM files - **TLS support** — optional `wss://` via cert/key PEM files
- **Prometheus metrics** — connection counts, message totals, disconnections - **Prometheus metrics** — connection counts, message totals, disconnections
- **Configurable logging** — output to stdout, stderr, or file with level filtering
- **Graceful shutdown** — clean exit on SIGINT/SIGTERM with client notification - **Graceful shutdown** — clean exit on SIGINT/SIGTERM with client notification
- **Zero dependencies at runtime** — single static binary - **Zero dependencies at runtime** — single static binary
@ -41,10 +40,6 @@ server:
metrics: metrics:
enabled: true enabled: true
port: 9090 # Prometheus metrics at :9090/metrics port: 9090 # Prometheus metrics at :9090/metrics
logging:
output: stderr # stdout, stderr, or a file path
level: info # debug, info, warn, error
``` ```
Override the config file path with `--config-file`: Override the config file path with `--config-file`:
@ -53,33 +48,6 @@ Override the config file path with `--config-file`:
./websocket-relay --config-file=/etc/relay/config.yaml ./websocket-relay --config-file=/etc/relay/config.yaml
``` ```
## Logging
The `logging` section controls where and what the server logs:
| Field | Values | Default | Description |
|-------|--------|---------|-------------|
| `output` | `stdout`, `stderr`, or a file path | `stderr` | Log output destination |
| `level` | `debug`, `info`, `warn`, `error` | `info` | Minimum log level to output |
**Examples:**
```yaml
# Log everything to a file
logging:
output: /var/log/websocket-relay.log
level: debug
# Quiet mode — only warnings and errors to stderr
logging:
output: stderr
level: warn
```
Log messages are prefixed with the level: `[DEBUG]`, `[INFO]`, `[WARN]`, `[ERROR]`.
File output uses append mode (`O_APPEND`) so logs are preserved across restarts and safe for external log rotation tools.
## Usage ## Usage
Connect any WebSocket client to the server: Connect any WebSocket client to the server:
@ -142,11 +110,10 @@ websocket-relay/
├── internal/ ├── internal/
│ ├── config/config.go # YAML config loader │ ├── config/config.go # YAML config loader
│ ├── hub/hub.go # WebSocket hub, connection management, broadcast │ ├── hub/hub.go # WebSocket hub, connection management, broadcast
│ ├── logging/logging.go # Log output setup and leveled logger
│ └── metrics/metrics.go # Prometheus metric definitions │ └── metrics/metrics.go # Prometheus metric definitions
├── example/index.html # Browser P2P chat demo ├── example/index.html # Browser P2P chat demo
├── config.yaml # Runtime configuration ├── config.yaml # Runtime configuration
├── config.example.yaml # Example config with TLS and logging ├── config.example.yaml # Example config with TLS enabled
└── Makefile # Build, test, release commands └── Makefile # Build, test, release commands
``` ```

View File

@ -8,9 +8,3 @@ server:
metrics: metrics:
enabled: true enabled: true
port: 9090 port: 9090
logging:
# output: stdout, stderr, or a file path (default: stderr)
output: stderr
# level: debug, info, warn, error (default: info)
level: info

View File

@ -19,10 +19,6 @@ type Config struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
Port int `yaml:"port"` Port int `yaml:"port"`
} `yaml:"metrics"` } `yaml:"metrics"`
Logging struct {
Output string `yaml:"output"`
Level string `yaml:"level"`
} `yaml:"logging"`
} }
func Load(filename string) (*Config, error) { func Load(filename string) (*Config, error) {
@ -33,4 +29,4 @@ func Load(filename string) (*Config, error) {
var config Config var config Config
err = yaml.Unmarshal(data, &config) err = yaml.Unmarshal(data, &config)
return &config, err return &config, err
} }

View File

@ -42,94 +42,4 @@ func TestLoadFileNotFound(t *testing.T) {
if err == nil { if err == nil {
t.Error("Expected error for nonexistent file") t.Error("Expected error for nonexistent file")
} }
} }
func TestLoadLoggingOutput(t *testing.T) {
testConfig := `server:
port: 8443
logging:
output: /var/log/relay.log
level: debug`
tmpFile, err := os.CreateTemp("", "config_logging_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(testConfig); err != nil {
t.Fatal(err)
}
tmpFile.Close()
cfg, err := Load(tmpFile.Name())
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.Logging.Output != "/var/log/relay.log" {
t.Errorf("Expected output '/var/log/relay.log', got '%s'", cfg.Logging.Output)
}
if cfg.Logging.Level != "debug" {
t.Errorf("Expected level 'debug', got '%s'", cfg.Logging.Level)
}
}
func TestLoadLoggingDefaults(t *testing.T) {
testConfig := `server:
port: 8443`
tmpFile, err := os.CreateTemp("", "config_defaults_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(testConfig); err != nil {
t.Fatal(err)
}
tmpFile.Close()
cfg, err := Load(tmpFile.Name())
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.Logging.Output != "" {
t.Errorf("Expected empty output default, got '%s'", cfg.Logging.Output)
}
if cfg.Logging.Level != "" {
t.Errorf("Expected empty level default, got '%s'", cfg.Logging.Level)
}
}
func TestLoadLoggingStdout(t *testing.T) {
testConfig := `server:
port: 8443
logging:
output: stdout
level: warn`
tmpFile, err := os.CreateTemp("", "config_stdout_*.yaml")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(testConfig); err != nil {
t.Fatal(err)
}
tmpFile.Close()
cfg, err := Load(tmpFile.Name())
if err != nil {
t.Fatalf("Load failed: %v", err)
}
if cfg.Logging.Output != "stdout" {
t.Errorf("Expected output 'stdout', got '%s'", cfg.Logging.Output)
}
if cfg.Logging.Level != "warn" {
t.Errorf("Expected level 'warn', got '%s'", cfg.Logging.Level)
}
}

View File

@ -1,11 +1,11 @@
package hub package hub
import ( import (
"log"
"net/http" "net/http"
"sync" "sync"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"websocket-relay/internal/logging"
"websocket-relay/internal/metrics" "websocket-relay/internal/metrics"
) )
@ -20,17 +20,15 @@ type Hub struct {
unregister chan *websocket.Conn unregister chan *websocket.Conn
stop chan struct{} stop chan struct{}
mu sync.RWMutex mu sync.RWMutex
logger *logging.Logger
} }
func New(logger *logging.Logger) *Hub { func New() *Hub {
return &Hub{ return &Hub{
clients: make(map[*websocket.Conn]bool), clients: make(map[*websocket.Conn]bool),
broadcast: make(chan []byte), broadcast: make(chan []byte),
register: make(chan *websocket.Conn), register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn), unregister: make(chan *websocket.Conn),
stop: make(chan struct{}), stop: make(chan struct{}),
logger: logger,
} }
} }
@ -47,7 +45,7 @@ func (h *Hub) Run() {
} }
h.mu.Unlock() h.mu.Unlock()
metrics.ConnectedClients.Set(0) metrics.ConnectedClients.Set(0)
h.logger.Info("Hub stopped, all clients disconnected") log.Printf("Hub stopped, all clients disconnected")
return return
case conn := <-h.register: case conn := <-h.register:
@ -56,7 +54,7 @@ func (h *Hub) Run() {
h.mu.Unlock() h.mu.Unlock()
metrics.ConnectedClients.Set(float64(len(h.clients))) metrics.ConnectedClients.Set(float64(len(h.clients)))
metrics.ConnectionsTotal.Inc() metrics.ConnectionsTotal.Inc()
h.logger.Infof("Client connected. Total: %d", len(h.clients)) log.Printf("Client connected. Total: %d", len(h.clients))
case conn := <-h.unregister: case conn := <-h.unregister:
h.mu.Lock() h.mu.Lock()
@ -67,7 +65,7 @@ func (h *Hub) Run() {
h.mu.Unlock() h.mu.Unlock()
metrics.ConnectedClients.Set(float64(len(h.clients))) metrics.ConnectedClients.Set(float64(len(h.clients)))
metrics.DisconnectionsTotal.Inc() metrics.DisconnectionsTotal.Inc()
h.logger.Infof("Client disconnected. Total: %d", len(h.clients)) log.Printf("Client disconnected. Total: %d", len(h.clients))
case message := <-h.broadcast: case message := <-h.broadcast:
metrics.MessagesTotal.Inc() metrics.MessagesTotal.Inc()
@ -88,7 +86,7 @@ func (h *Hub) Run() {
conn.Close() conn.Close()
metrics.ConnectedClients.Set(float64(len(h.clients))) metrics.ConnectedClients.Set(float64(len(h.clients)))
metrics.DisconnectionsTotal.Inc() metrics.DisconnectionsTotal.Inc()
h.logger.Warnf("Client disconnected (write error). Total: %d", len(h.clients)) log.Printf("Client disconnected (write error). Total: %d", len(h.clients))
} }
h.mu.Unlock() h.mu.Unlock()
} }
@ -104,7 +102,7 @@ func (h *Hub) Shutdown() {
func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) { func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
h.logger.Errorf("WebSocket upgrade error: %v", err) log.Printf("WebSocket upgrade error: %v", err)
return return
} }

View File

@ -1,7 +1,6 @@
package hub package hub
import ( import (
"bytes"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@ -10,14 +9,12 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"websocket-relay/internal/logging"
) )
// helper: start a test server with a running Hub, return the server and hub // helper: start a test server with a running Hub, return the server and hub
func setupTestServer(t *testing.T) (*httptest.Server, *Hub) { func setupTestServer(t *testing.T) (*httptest.Server, *Hub) {
t.Helper() t.Helper()
logger := logging.NewLogger("debug", &bytes.Buffer{}) h := New()
h := New(logger)
go h.Run() go h.Run()
server := httptest.NewServer(http.HandlerFunc(h.HandleWebSocket)) server := httptest.NewServer(http.HandlerFunc(h.HandleWebSocket))

View File

@ -1,19 +1,12 @@
package hub package hub
import ( import (
"bytes"
"testing" "testing"
"time" "time"
"websocket-relay/internal/logging"
) )
func newTestLogger() *logging.Logger {
return logging.NewLogger("debug", &bytes.Buffer{})
}
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
h := New(newTestLogger()) h := New()
if h == nil { if h == nil {
t.Fatal("New returned nil") t.Fatal("New returned nil")
} }
@ -29,7 +22,7 @@ func TestNew(t *testing.T) {
} }
func TestClientCount(t *testing.T) { func TestClientCount(t *testing.T) {
h := New(newTestLogger()) h := New()
go h.Run() go h.Run()
defer h.Shutdown() defer h.Shutdown()
@ -39,7 +32,7 @@ func TestClientCount(t *testing.T) {
} }
func TestBroadcastChannel(t *testing.T) { func TestBroadcastChannel(t *testing.T) {
h := New(newTestLogger()) h := New()
go h.Run() go h.Run()
defer h.Shutdown() defer h.Shutdown()
@ -52,7 +45,7 @@ func TestBroadcastChannel(t *testing.T) {
} }
func TestShutdown(t *testing.T) { func TestShutdown(t *testing.T) {
h := New(newTestLogger()) h := New()
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {

View File

@ -1,126 +0,0 @@
package logging
import (
"fmt"
"io"
"log"
"os"
"strings"
)
// Level represents the severity of a log message.
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
)
// Logger provides level-aware logging.
type Logger struct {
level Level
logger *log.Logger
}
// Setup configures the global log output destination.
// Returns the opened file (if a file path was given) so the caller can defer Close.
// For "stdout", "stderr", or empty string, returns nil (no file to close).
func Setup(output string) (*os.File, error) {
switch strings.ToLower(output) {
case "", "stderr":
log.SetOutput(os.Stderr)
return nil, nil
case "stdout":
log.SetOutput(os.Stdout)
return nil, nil
default:
file, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file %s: %w", output, err)
}
log.SetOutput(file)
return file, nil
}
}
// NewLogger creates a new Logger with the given level and output writer.
// If level is empty or invalid, defaults to LevelInfo.
func NewLogger(level string, output io.Writer) *Logger {
return &Logger{
level: parseLevel(level),
logger: log.New(output, "", log.LstdFlags),
}
}
// Debug logs a message at debug level.
func (l *Logger) Debug(msg string) {
if l.level <= LevelDebug {
l.logger.Printf("[DEBUG] %s", msg)
}
}
// Debugf logs a formatted message at debug level.
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.level <= LevelDebug {
l.logger.Printf("[DEBUG] "+format, args...)
}
}
// Info logs a message at info level.
func (l *Logger) Info(msg string) {
if l.level <= LevelInfo {
l.logger.Printf("[INFO] %s", msg)
}
}
// Infof logs a formatted message at info level.
func (l *Logger) Infof(format string, args ...interface{}) {
if l.level <= LevelInfo {
l.logger.Printf("[INFO] "+format, args...)
}
}
// Warn logs a message at warn level.
func (l *Logger) Warn(msg string) {
if l.level <= LevelWarn {
l.logger.Printf("[WARN] %s", msg)
}
}
// Warnf logs a formatted message at warn level.
func (l *Logger) Warnf(format string, args ...interface{}) {
if l.level <= LevelWarn {
l.logger.Printf("[WARN] "+format, args...)
}
}
// Error logs a message at error level.
func (l *Logger) Error(msg string) {
if l.level <= LevelError {
l.logger.Printf("[ERROR] %s", msg)
}
}
// Errorf logs a formatted message at error level.
func (l *Logger) Errorf(format string, args ...interface{}) {
if l.level <= LevelError {
l.logger.Printf("[ERROR] "+format, args...)
}
}
func parseLevel(level string) Level {
switch strings.ToLower(level) {
case "debug":
return LevelDebug
case "info":
return LevelInfo
case "warn":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
}

View File

@ -1,278 +0,0 @@
package logging
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestSetupStdout(t *testing.T) {
file, err := Setup("stdout")
if err != nil {
t.Fatalf("Setup(stdout) failed: %v", err)
}
if file != nil {
t.Error("Expected nil file for stdout")
}
}
func TestSetupStderr(t *testing.T) {
file, err := Setup("stderr")
if err != nil {
t.Fatalf("Setup(stderr) failed: %v", err)
}
if file != nil {
t.Error("Expected nil file for stderr")
}
}
func TestSetupEmpty(t *testing.T) {
file, err := Setup("")
if err != nil {
t.Fatalf("Setup('') failed: %v", err)
}
if file != nil {
t.Error("Expected nil file for empty string")
}
}
func TestSetupFilePath(t *testing.T) {
tmpDir := t.TempDir()
logPath := filepath.Join(tmpDir, "test.log")
file, err := Setup(logPath)
if err != nil {
t.Fatalf("Setup(file) failed: %v", err)
}
if file == nil {
t.Fatal("Expected non-nil file for file path")
}
defer file.Close()
// Verify file was created
if _, err := os.Stat(logPath); os.IsNotExist(err) {
t.Error("Expected log file to be created")
}
}
func TestSetupInvalidPath(t *testing.T) {
_, err := Setup("/nonexistent/directory/path/test.log")
if err == nil {
t.Error("Expected error for invalid path")
}
}
func TestNewLoggerDefaultLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("", &buf)
if logger.level != LevelInfo {
t.Errorf("Expected default level Info, got %d", logger.level)
}
}
func TestNewLoggerInvalidLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("invalid", &buf)
if logger.level != LevelInfo {
t.Errorf("Expected default level Info for invalid input, got %d", logger.level)
}
}
func TestNewLoggerDebugLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("debug", &buf)
if logger.level != LevelDebug {
t.Errorf("Expected level Debug, got %d", logger.level)
}
}
func TestNewLoggerWarnLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("warn", &buf)
if logger.level != LevelWarn {
t.Errorf("Expected level Warn, got %d", logger.level)
}
}
func TestNewLoggerErrorLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("error", &buf)
if logger.level != LevelError {
t.Errorf("Expected level Error, got %d", logger.level)
}
}
func TestLoggerDebugOutputAtDebugLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("debug", &buf)
logger.Debug("test message")
output := buf.String()
if !strings.Contains(output, "[DEBUG]") {
t.Errorf("Expected [DEBUG] prefix, got: %s", output)
}
if !strings.Contains(output, "test message") {
t.Errorf("Expected 'test message' in output, got: %s", output)
}
}
func TestLoggerDebugSuppressedAtInfoLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("info", &buf)
logger.Debug("should not appear")
output := buf.String()
if output != "" {
t.Errorf("Expected no output for debug at info level, got: %s", output)
}
}
func TestLoggerInfoOutputAtInfoLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("info", &buf)
logger.Info("info message")
output := buf.String()
if !strings.Contains(output, "[INFO]") {
t.Errorf("Expected [INFO] prefix, got: %s", output)
}
if !strings.Contains(output, "info message") {
t.Errorf("Expected 'info message' in output, got: %s", output)
}
}
func TestLoggerInfoSuppressedAtWarnLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("warn", &buf)
logger.Info("should not appear")
output := buf.String()
if output != "" {
t.Errorf("Expected no output for info at warn level, got: %s", output)
}
}
func TestLoggerWarnOutputAtWarnLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("warn", &buf)
logger.Warn("warn message")
output := buf.String()
if !strings.Contains(output, "[WARN]") {
t.Errorf("Expected [WARN] prefix, got: %s", output)
}
if !strings.Contains(output, "warn message") {
t.Errorf("Expected 'warn message' in output, got: %s", output)
}
}
func TestLoggerWarnSuppressedAtErrorLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("error", &buf)
logger.Warn("should not appear")
output := buf.String()
if output != "" {
t.Errorf("Expected no output for warn at error level, got: %s", output)
}
}
func TestLoggerErrorOutputAtErrorLevel(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("error", &buf)
logger.Error("error message")
output := buf.String()
if !strings.Contains(output, "[ERROR]") {
t.Errorf("Expected [ERROR] prefix, got: %s", output)
}
if !strings.Contains(output, "error message") {
t.Errorf("Expected 'error message' in output, got: %s", output)
}
}
func TestLoggerErrorAlwaysOutputs(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("error", &buf)
logger.Error("critical failure")
output := buf.String()
if !strings.Contains(output, "critical failure") {
t.Errorf("Expected error message in output, got: %s", output)
}
}
func TestLoggerDebugLevelAllMessages(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("debug", &buf)
logger.Debug("d")
logger.Info("i")
logger.Warn("w")
logger.Error("e")
output := buf.String()
if !strings.Contains(output, "[DEBUG]") {
t.Error("Expected [DEBUG] in output")
}
if !strings.Contains(output, "[INFO]") {
t.Error("Expected [INFO] in output")
}
if !strings.Contains(output, "[WARN]") {
t.Error("Expected [WARN] in output")
}
if !strings.Contains(output, "[ERROR]") {
t.Error("Expected [ERROR] in output")
}
}
func TestLoggerFormatf(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("info", &buf)
logger.Infof("count: %d", 42)
output := buf.String()
if !strings.Contains(output, "count: 42") {
t.Errorf("Expected formatted output 'count: 42', got: %s", output)
}
}
func TestLoggerLevelCaseInsensitive(t *testing.T) {
var buf bytes.Buffer
logger := NewLogger("DEBUG", &buf)
if logger.level != LevelDebug {
t.Errorf("Expected level Debug for 'DEBUG', got %d", logger.level)
}
logger = NewLogger("Info", &buf)
if logger.level != LevelInfo {
t.Errorf("Expected level Info for 'Info', got %d", logger.level)
}
logger = NewLogger("WARN", &buf)
if logger.level != LevelWarn {
t.Errorf("Expected level Warn for 'WARN', got %d", logger.level)
}
logger = NewLogger("ERROR", &buf)
if logger.level != LevelError {
t.Errorf("Expected level Error for 'ERROR', got %d", logger.level)
}
}

37
main.go
View File

@ -13,7 +13,6 @@ import (
"websocket-relay/internal/config" "websocket-relay/internal/config"
"websocket-relay/internal/hub" "websocket-relay/internal/hub"
"websocket-relay/internal/logging"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
@ -27,19 +26,7 @@ func main() {
log.Fatal("Failed to load config:", err) log.Fatal("Failed to load config:", err)
} }
// Setup log output destination h := hub.New()
logFile, err := logging.Setup(cfg.Logging.Output)
if err != nil {
log.Fatal("Failed to setup logging:", err)
}
if logFile != nil {
defer logFile.Close()
}
// Create leveled logger
logger := logging.NewLogger(cfg.Logging.Level, log.Writer())
h := hub.New(logger)
go h.Run() go h.Run()
// Start metrics server if enabled // Start metrics server if enabled
@ -53,9 +40,9 @@ func main() {
Handler: metricsMux, Handler: metricsMux,
} }
go func() { go func() {
logger.Infof("Metrics server starting on %s", metricsAddr) log.Printf("Metrics server starting on %s", metricsAddr)
if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := metricsServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Errorf("Metrics server error: %v", err) log.Printf("Metrics server error: %v", err)
} }
}() }()
} }
@ -72,16 +59,14 @@ func main() {
// Start the main server in a goroutine // Start the main server in a goroutine
go func() { go func() {
if cfg.Server.TLS.Enabled { if cfg.Server.TLS.Enabled {
logger.Infof("WebSocket relay server starting on %s (TLS)", addr) log.Printf("WebSocket relay server starting on %s (TLS)", addr)
if err := server.ListenAndServeTLS(cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile); err != nil && err != http.ErrServerClosed { if err := server.ListenAndServeTLS(cfg.Server.TLS.CertFile, cfg.Server.TLS.KeyFile); err != nil && err != http.ErrServerClosed {
logger.Errorf("Server error: %v", err) log.Fatalf("Server error: %v", err)
os.Exit(1)
} }
} else { } else {
logger.Infof("WebSocket relay server starting on %s (HTTP)", addr) log.Printf("WebSocket relay server starting on %s (HTTP)", addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Errorf("Server error: %v", err) log.Fatalf("Server error: %v", err)
os.Exit(1)
} }
} }
}() }()
@ -90,7 +75,7 @@ func main() {
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
sig := <-quit sig := <-quit
logger.Infof("Received signal %v, shutting down gracefully...", sig) log.Printf("Received signal %v, shutting down gracefully...", sig)
// Create a deadline for the shutdown // Create a deadline for the shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@ -98,18 +83,18 @@ func main() {
// Shut down the main HTTP server (stops accepting new connections) // Shut down the main HTTP server (stops accepting new connections)
if err := server.Shutdown(ctx); err != nil { if err := server.Shutdown(ctx); err != nil {
logger.Errorf("HTTP server shutdown error: %v", err) log.Printf("HTTP server shutdown error: %v", err)
} }
// Shut down the metrics server // Shut down the metrics server
if metricsServer != nil { if metricsServer != nil {
if err := metricsServer.Shutdown(ctx); err != nil { if err := metricsServer.Shutdown(ctx); err != nil {
logger.Errorf("Metrics server shutdown error: %v", err) log.Printf("Metrics server shutdown error: %v", err)
} }
} }
// Stop the hub and close all WebSocket connections // Stop the hub and close all WebSocket connections
h.Shutdown() h.Shutdown()
logger.Info("Server stopped") log.Printf("Server stopped")
} }