perubahan
This commit is contained in:
@@ -23,6 +23,7 @@ type Config struct {
|
||||
Bpjs BpjsConfig
|
||||
SatuSehat SatuSehatConfig
|
||||
Swagger SwaggerConfig
|
||||
WebSocket WebSocketConfig // Tambahkan ini
|
||||
Validator *validator.Validate
|
||||
}
|
||||
|
||||
@@ -90,6 +91,38 @@ type SatuSehatConfig struct {
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
}
|
||||
|
||||
type WebSocketConfig struct {
|
||||
// Timeout configurations
|
||||
ReadTimeout time.Duration `json:"read_timeout"`
|
||||
WriteTimeout time.Duration `json:"write_timeout"`
|
||||
PingInterval time.Duration `json:"ping_interval"`
|
||||
PongTimeout time.Duration `json:"pong_timeout"`
|
||||
HandshakeTimeout time.Duration `json:"handshake_timeout"`
|
||||
|
||||
// Buffer sizes
|
||||
ReadBufferSize int `json:"read_buffer_size"`
|
||||
WriteBufferSize int `json:"write_buffer_size"`
|
||||
ChannelBufferSize int `json:"channel_buffer_size"`
|
||||
MessageQueueSize int `json:"message_queue_size"`
|
||||
|
||||
// Connection limits
|
||||
MaxMessageSize int `json:"max_message_size"`
|
||||
QueueWorkers int `json:"queue_workers"`
|
||||
|
||||
// Monitoring
|
||||
ActivityLogSize int `json:"activity_log_size"`
|
||||
CleanupInterval time.Duration `json:"cleanup_interval"`
|
||||
InactiveTimeout time.Duration `json:"inactive_timeout"`
|
||||
|
||||
// Server info
|
||||
ServerID string `json:"server_id"`
|
||||
|
||||
// Features
|
||||
EnableCompression bool `json:"enable_compression"`
|
||||
EnableMetrics bool `json:"enable_metrics"`
|
||||
EnableMonitoring bool `json:"enable_monitoring"`
|
||||
}
|
||||
|
||||
// SetHeader generates required headers for BPJS VClaim API
|
||||
// func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
|
||||
// timenow := time.Now().UTC()
|
||||
@@ -150,18 +183,23 @@ func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) {
|
||||
|
||||
func LoadConfig() *Config {
|
||||
config := &Config{
|
||||
// Configuration for the server
|
||||
Server: ServerConfig{
|
||||
Port: getEnvAsInt("PORT", 8080),
|
||||
Mode: getEnv("GIN_MODE", "debug"),
|
||||
},
|
||||
Databases: make(map[string]DatabaseConfig),
|
||||
// Configuration for the database
|
||||
Databases: make(map[string]DatabaseConfig),
|
||||
// Configuration for read replicas
|
||||
ReadReplicas: make(map[string][]DatabaseConfig),
|
||||
// Configuration for Keycloak authentication and authorization
|
||||
Keycloak: KeycloakConfig{
|
||||
Issuer: getEnv("KEYCLOAK_ISSUER", "https://keycloak.example.com/auth/realms/yourrealm"),
|
||||
Audience: getEnv("KEYCLOAK_AUDIENCE", "your-client-id"),
|
||||
JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"),
|
||||
Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true),
|
||||
},
|
||||
// Configuration for BPJS service bridging API
|
||||
Bpjs: BpjsConfig{
|
||||
BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"),
|
||||
ConsID: getEnv("BPJS_CONSID", ""),
|
||||
@@ -169,6 +207,7 @@ func LoadConfig() *Config {
|
||||
SecretKey: getEnv("BPJS_SECRETKEY", ""),
|
||||
Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")),
|
||||
},
|
||||
// Configuration for Satu Sehat service bridging API
|
||||
SatuSehat: SatuSehatConfig{
|
||||
OrgID: getEnv("BRIDGING_SATUSEHAT_ORG_ID", ""),
|
||||
FasyakesID: getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", ""),
|
||||
@@ -180,6 +219,39 @@ func LoadConfig() *Config {
|
||||
KFAURL: getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2"),
|
||||
Timeout: parseDuration(getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s")),
|
||||
},
|
||||
// Configuration for WebSocket server
|
||||
WebSocket: WebSocketConfig{ // Tambahkan ini
|
||||
// Timeout configurations
|
||||
ReadTimeout: parseDuration(getEnv("WS_READ_TIMEOUT", "300s")),
|
||||
WriteTimeout: parseDuration(getEnv("WS_WRITE_TIMEOUT", "30s")),
|
||||
PingInterval: parseDuration(getEnv("WS_PING_INTERVAL", "60s")),
|
||||
PongTimeout: parseDuration(getEnv("WS_PONG_TIMEOUT", "70s")),
|
||||
HandshakeTimeout: parseDuration(getEnv("WS_HANDSHAKE_TIMEOUT", "45s")),
|
||||
|
||||
// Buffer sizes
|
||||
ReadBufferSize: getEnvAsInt("WS_READ_BUFFER_SIZE", 8192),
|
||||
WriteBufferSize: getEnvAsInt("WS_WRITE_BUFFER_SIZE", 8192),
|
||||
ChannelBufferSize: getEnvAsInt("WS_CHANNEL_BUFFER_SIZE", 512),
|
||||
MessageQueueSize: getEnvAsInt("WS_MESSAGE_QUEUE_SIZE", 5000),
|
||||
|
||||
// Connection limits
|
||||
MaxMessageSize: getEnvAsInt("WS_MAX_MESSAGE_SIZE", 8192),
|
||||
QueueWorkers: getEnvAsInt("WS_QUEUE_WORKERS", 10),
|
||||
|
||||
// Monitoring
|
||||
ActivityLogSize: getEnvAsInt("WS_ACTIVITY_LOG_SIZE", 1000),
|
||||
CleanupInterval: parseDuration(getEnv("WS_CLEANUP_INTERVAL", "2m")),
|
||||
InactiveTimeout: parseDuration(getEnv("WS_INACTIVE_TIMEOUT", "5m")),
|
||||
|
||||
// Server info
|
||||
ServerID: getEnv("WS_SERVER_ID", "api-service-v1"),
|
||||
|
||||
// Features
|
||||
EnableCompression: getEnvAsBool("WS_ENABLE_COMPRESSION", true),
|
||||
EnableMetrics: getEnvAsBool("WS_ENABLE_METRICS", true),
|
||||
EnableMonitoring: getEnvAsBool("WS_ENABLE_MONITORING", true),
|
||||
},
|
||||
// Configuration for Swagger
|
||||
Swagger: SwaggerConfig{
|
||||
Title: getEnv("SWAGGER_TITLE", "SERVICE API"),
|
||||
Description: getEnv("SWAGGER_DESCRIPTION", "CUSTUM SERVICE API"),
|
||||
@@ -735,5 +807,193 @@ func (c *Config) Validate() error {
|
||||
log.Fatal("SatuSehat Base URL is required")
|
||||
}
|
||||
|
||||
// Validate WebSocket configuration
|
||||
if c.WebSocket.ReadTimeout <= 0 {
|
||||
log.Fatal("WebSocket Read Timeout must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.WriteTimeout <= 0 {
|
||||
log.Fatal("WebSocket Write Timeout must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.PingInterval <= 0 {
|
||||
log.Fatal("WebSocket Ping Interval must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.PongTimeout <= 0 {
|
||||
log.Fatal("WebSocket Pong Timeout must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.ReadBufferSize <= 0 {
|
||||
log.Fatal("WebSocket Read Buffer Size must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.WriteBufferSize <= 0 {
|
||||
log.Fatal("WebSocket Write Buffer Size must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.ChannelBufferSize <= 0 {
|
||||
log.Fatal("WebSocket Channel Buffer Size must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.MessageQueueSize <= 0 {
|
||||
log.Fatal("WebSocket Message Queue Size must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.MaxMessageSize <= 0 {
|
||||
log.Fatal("WebSocket Max Message Size must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.QueueWorkers <= 0 {
|
||||
log.Fatal("WebSocket Queue Workers must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.ActivityLogSize <= 0 {
|
||||
log.Fatal("WebSocket Activity Log Size must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.CleanupInterval <= 0 {
|
||||
log.Fatal("WebSocket Cleanup Interval must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.InactiveTimeout <= 0 {
|
||||
log.Fatal("WebSocket Inactive Timeout must be greater than 0")
|
||||
}
|
||||
if c.WebSocket.ServerID == "" {
|
||||
log.Fatal("WebSocket Server ID is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//** WebSocket **//
|
||||
|
||||
// DefaultWebSocketConfig mengembalikan konfigurasi default untuk WebSocket
|
||||
func DefaultWebSocketConfig() WebSocketConfig {
|
||||
return WebSocketConfig{
|
||||
// Timeout configurations
|
||||
ReadTimeout: 300 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
PingInterval: 60 * time.Second,
|
||||
PongTimeout: 70 * time.Second,
|
||||
HandshakeTimeout: 45 * time.Second,
|
||||
|
||||
// Buffer sizes
|
||||
ReadBufferSize: 8192,
|
||||
WriteBufferSize: 8192,
|
||||
ChannelBufferSize: 512,
|
||||
MessageQueueSize: 5000,
|
||||
|
||||
// Connection limits
|
||||
MaxMessageSize: 8192,
|
||||
QueueWorkers: 10,
|
||||
|
||||
// Monitoring
|
||||
ActivityLogSize: 1000,
|
||||
CleanupInterval: 2 * time.Minute,
|
||||
InactiveTimeout: 5 * time.Minute,
|
||||
|
||||
// Server info
|
||||
ServerID: "api-service-v1",
|
||||
|
||||
// Features
|
||||
EnableCompression: true,
|
||||
EnableMetrics: true,
|
||||
EnableMonitoring: true,
|
||||
}
|
||||
}
|
||||
|
||||
// HighPerformanceWebSocketConfig mengembalikan konfigurasi untuk performa tinggi
|
||||
func HighPerformanceWebSocketConfig() WebSocketConfig {
|
||||
return WebSocketConfig{
|
||||
// Timeout configurations
|
||||
ReadTimeout: 300 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
PingInterval: 30 * time.Second,
|
||||
PongTimeout: 40 * time.Second,
|
||||
HandshakeTimeout: 30 * time.Second,
|
||||
|
||||
// Buffer sizes
|
||||
ReadBufferSize: 16384,
|
||||
WriteBufferSize: 16384,
|
||||
ChannelBufferSize: 1024,
|
||||
MessageQueueSize: 10000,
|
||||
|
||||
// Connection limits
|
||||
MaxMessageSize: 16384,
|
||||
QueueWorkers: 20,
|
||||
|
||||
// Monitoring
|
||||
ActivityLogSize: 2000,
|
||||
CleanupInterval: 1 * time.Minute,
|
||||
InactiveTimeout: 3 * time.Minute,
|
||||
|
||||
// Server info
|
||||
ServerID: "api-service-hp",
|
||||
|
||||
// Features
|
||||
EnableCompression: true,
|
||||
EnableMetrics: true,
|
||||
EnableMonitoring: true,
|
||||
}
|
||||
}
|
||||
|
||||
// LowResourceWebSocketConfig mengembalikan konfigurasi untuk sumber daya terbatas
|
||||
func LowResourceWebSocketConfig() WebSocketConfig {
|
||||
return WebSocketConfig{
|
||||
// Timeout configurations
|
||||
ReadTimeout: 300 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
PingInterval: 120 * time.Second,
|
||||
PongTimeout: 130 * time.Second,
|
||||
HandshakeTimeout: 60 * time.Second,
|
||||
|
||||
// Buffer sizes
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
ChannelBufferSize: 256,
|
||||
MessageQueueSize: 2500,
|
||||
|
||||
// Connection limits
|
||||
MaxMessageSize: 4096,
|
||||
QueueWorkers: 5,
|
||||
|
||||
// Monitoring
|
||||
ActivityLogSize: 500,
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
InactiveTimeout: 10 * time.Minute,
|
||||
|
||||
// Server info
|
||||
ServerID: "api-service-lr",
|
||||
|
||||
// Features
|
||||
EnableCompression: false,
|
||||
EnableMetrics: false,
|
||||
EnableMonitoring: true,
|
||||
}
|
||||
}
|
||||
|
||||
// CustomWebSocketConfig memungkinkan kustomisasi konfigurasi
|
||||
func CustomWebSocketConfig(
|
||||
readTimeout, writeTimeout, pingInterval, pongTimeout time.Duration,
|
||||
readBufferSize, writeBufferSize, channelBufferSize, messageQueueSize int,
|
||||
maxMessageSize, queueWorkers int,
|
||||
activityLogSize int,
|
||||
cleanupInterval, inactiveTimeout time.Duration,
|
||||
serverID string,
|
||||
enableCompression, enableMetrics, enableMonitoring bool,
|
||||
) WebSocketConfig {
|
||||
return WebSocketConfig{
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
PingInterval: pingInterval,
|
||||
PongTimeout: pongTimeout,
|
||||
HandshakeTimeout: 45 * time.Second,
|
||||
|
||||
ReadBufferSize: readBufferSize,
|
||||
WriteBufferSize: writeBufferSize,
|
||||
ChannelBufferSize: channelBufferSize,
|
||||
MessageQueueSize: messageQueueSize,
|
||||
|
||||
MaxMessageSize: maxMessageSize,
|
||||
QueueWorkers: queueWorkers,
|
||||
|
||||
ActivityLogSize: activityLogSize,
|
||||
CleanupInterval: cleanupInterval,
|
||||
InactiveTimeout: inactiveTimeout,
|
||||
|
||||
ServerID: serverID,
|
||||
|
||||
EnableCompression: enableCompression,
|
||||
EnableMetrics: enableMetrics,
|
||||
EnableMonitoring: enableMonitoring,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WebSocketBroadcaster defines the interface for broadcasting messages
|
||||
type WebSocketBroadcaster interface {
|
||||
BroadcastMessage(messageType string, data interface{})
|
||||
}
|
||||
|
||||
// Broadcaster handles server-initiated broadcasts to WebSocket clients
|
||||
type Broadcaster struct {
|
||||
handler WebSocketBroadcaster
|
||||
tickers []*time.Ticker
|
||||
quit chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewBroadcaster creates a new Broadcaster instance
|
||||
func NewBroadcaster(handler WebSocketBroadcaster) *Broadcaster {
|
||||
return &Broadcaster{
|
||||
handler: handler,
|
||||
tickers: make([]*time.Ticker, 0),
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// StartHeartbeat starts sending periodic heartbeat messages to all clients
|
||||
func (b *Broadcaster) StartHeartbeat(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
b.tickers = append(b.tickers, ticker)
|
||||
go func() {
|
||||
defer func() {
|
||||
// Remove ticker from slice when done
|
||||
for i, t := range b.tickers {
|
||||
if t == ticker {
|
||||
b.tickers = append(b.tickers[:i], b.tickers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.handler.BroadcastMessage("heartbeat", map[string]interface{}{
|
||||
"message": "Server heartbeat",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
case <-b.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the broadcaster
|
||||
func (b *Broadcaster) Stop() {
|
||||
close(b.quit)
|
||||
for _, ticker := range b.tickers {
|
||||
if ticker != nil {
|
||||
ticker.Stop()
|
||||
}
|
||||
}
|
||||
b.tickers = nil
|
||||
}
|
||||
|
||||
// BroadcastNotification sends a notification message to all clients
|
||||
func (b *Broadcaster) BroadcastNotification(title, message, level string) {
|
||||
b.handler.BroadcastMessage("notification", map[string]interface{}{
|
||||
"title": title,
|
||||
"message": message,
|
||||
"level": level,
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// SimulateDataStream simulates streaming data to clients (useful for demos)
|
||||
func (b *Broadcaster) SimulateDataStream() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
b.tickers = append(b.tickers, ticker)
|
||||
go func() {
|
||||
defer func() {
|
||||
// Remove ticker from slice when done
|
||||
for i, t := range b.tickers {
|
||||
if t == ticker {
|
||||
b.tickers = append(b.tickers[:i], b.tickers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
counter := 0
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
counter++
|
||||
b.handler.BroadcastMessage("data_stream", map[string]interface{}{
|
||||
"id": counter,
|
||||
"value": counter * 10,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"type": "simulated_data",
|
||||
})
|
||||
case <-b.quit:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockWebSocketHandler is a mock implementation for testing
|
||||
type MockWebSocketHandler struct {
|
||||
mu sync.Mutex
|
||||
messages []map[string]interface{}
|
||||
broadcasts []string
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) BroadcastMessage(messageType string, data interface{}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.broadcasts = append(m.broadcasts, messageType)
|
||||
m.messages = append(m.messages, map[string]interface{}{
|
||||
"type": messageType,
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) GetMessages() []map[string]interface{} {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
result := make([]map[string]interface{}, len(m.messages))
|
||||
copy(result, m.messages)
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) GetBroadcasts() []string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
result := make([]string, len(m.broadcasts))
|
||||
copy(result, m.broadcasts)
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *MockWebSocketHandler) Clear() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.messages = make([]map[string]interface{}, 0)
|
||||
m.broadcasts = make([]string, 0)
|
||||
}
|
||||
|
||||
func NewMockWebSocketHandler() *MockWebSocketHandler {
|
||||
return &MockWebSocketHandler{
|
||||
messages: make([]map[string]interface{}, 0),
|
||||
broadcasts: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroadcaster_StartHeartbeat(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start heartbeat with short interval for testing
|
||||
broadcaster.StartHeartbeat(100 * time.Millisecond)
|
||||
|
||||
// Wait for a few heartbeats
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
|
||||
// Stop the broadcaster
|
||||
broadcaster.Stop()
|
||||
|
||||
// Check if heartbeats were sent
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected heartbeat messages, but got none")
|
||||
}
|
||||
|
||||
// Check that all messages are heartbeat type
|
||||
broadcasts := mockHandler.GetBroadcasts()
|
||||
for _, msgType := range broadcasts {
|
||||
if msgType != "heartbeat" {
|
||||
t.Errorf("Expected heartbeat message type, got %s", msgType)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Received %d heartbeat messages", len(messages))
|
||||
}
|
||||
|
||||
func TestBroadcaster_BroadcastNotification(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Send a notification
|
||||
broadcaster.BroadcastNotification("Test Title", "Test Message", "info")
|
||||
|
||||
// Check if notification was sent
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) != 1 {
|
||||
t.Errorf("Expected 1 message, got %d", len(messages))
|
||||
return
|
||||
}
|
||||
|
||||
msg := messages[0]
|
||||
if msg["type"] != "notification" {
|
||||
t.Errorf("Expected message type 'notification', got %s", msg["type"])
|
||||
}
|
||||
|
||||
data := msg["data"].(map[string]interface{})
|
||||
if data["title"] != "Test Title" {
|
||||
t.Errorf("Expected title 'Test Title', got %s", data["title"])
|
||||
}
|
||||
if data["message"] != "Test Message" {
|
||||
t.Errorf("Expected message 'Test Message', got %s", data["message"])
|
||||
}
|
||||
if data["level"] != "info" {
|
||||
t.Errorf("Expected level 'info', got %s", data["level"])
|
||||
}
|
||||
|
||||
t.Logf("Notification sent successfully: %+v", data)
|
||||
}
|
||||
|
||||
func TestBroadcaster_SimulateDataStream(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start data stream with short interval for testing
|
||||
broadcaster.SimulateDataStream()
|
||||
|
||||
// Wait for a few data points
|
||||
time.Sleep(550 * time.Millisecond)
|
||||
|
||||
// Stop the broadcaster
|
||||
broadcaster.Stop()
|
||||
|
||||
// Check if data stream messages were sent
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected data stream messages, but got none")
|
||||
}
|
||||
|
||||
// Check that all messages are data_stream type
|
||||
broadcasts := mockHandler.GetBroadcasts()
|
||||
for _, msgType := range broadcasts {
|
||||
if msgType != "data_stream" {
|
||||
t.Errorf("Expected data_stream message type, got %s", msgType)
|
||||
}
|
||||
}
|
||||
|
||||
// Check data structure
|
||||
for i, msg := range messages {
|
||||
data := msg["data"].(map[string]interface{})
|
||||
if data["type"] != "simulated_data" {
|
||||
t.Errorf("Expected data type 'simulated_data', got %s", data["type"])
|
||||
}
|
||||
if id, ok := data["id"].(int); ok {
|
||||
if id != i+1 {
|
||||
t.Errorf("Expected id %d, got %d", i+1, id)
|
||||
}
|
||||
}
|
||||
if value, ok := data["value"].(int); ok {
|
||||
expectedValue := (i + 1) * 10
|
||||
if value != expectedValue {
|
||||
t.Errorf("Expected value %d, got %d", expectedValue, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Received %d data stream messages", len(messages))
|
||||
}
|
||||
|
||||
func TestBroadcaster_Stop(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start heartbeat
|
||||
broadcaster.StartHeartbeat(50 * time.Millisecond)
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Stop the broadcaster
|
||||
broadcaster.Stop()
|
||||
|
||||
// Clear previous messages
|
||||
mockHandler.Clear()
|
||||
|
||||
// Wait a bit more to ensure no new messages are sent
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Check that no new messages were sent after stopping
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) > 0 {
|
||||
t.Errorf("Expected no messages after stopping, but got %d", len(messages))
|
||||
}
|
||||
|
||||
// Clear quit channel to allow reuse in tests
|
||||
broadcaster.quit = make(chan struct{})
|
||||
|
||||
t.Log("Broadcaster stopped successfully")
|
||||
}
|
||||
|
||||
func TestBroadcaster_MultipleOperations(t *testing.T) {
|
||||
mockHandler := NewMockWebSocketHandler()
|
||||
broadcaster := NewBroadcaster(mockHandler)
|
||||
|
||||
// Start heartbeat
|
||||
broadcaster.StartHeartbeat(100 * time.Millisecond)
|
||||
|
||||
// Send notification
|
||||
broadcaster.BroadcastNotification("Test", "Message", "warning")
|
||||
|
||||
// Start data stream
|
||||
broadcaster.SimulateDataStream()
|
||||
|
||||
// Wait for some activity
|
||||
time.Sleep(350 * time.Millisecond)
|
||||
|
||||
// Stop everything
|
||||
broadcaster.Stop()
|
||||
|
||||
// Check results
|
||||
messages := mockHandler.GetMessages()
|
||||
if len(messages) == 0 {
|
||||
t.Error("Expected messages from multiple operations, but got none")
|
||||
}
|
||||
|
||||
broadcasts := mockHandler.GetBroadcasts()
|
||||
hasHeartbeat := false
|
||||
hasNotification := false
|
||||
hasDataStream := false
|
||||
|
||||
for _, msgType := range broadcasts {
|
||||
switch msgType {
|
||||
case "heartbeat":
|
||||
hasHeartbeat = true
|
||||
case "notification":
|
||||
hasNotification = true
|
||||
case "data_stream":
|
||||
hasDataStream = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasHeartbeat {
|
||||
t.Error("Expected heartbeat messages")
|
||||
}
|
||||
if !hasNotification {
|
||||
t.Error("Expected notification message")
|
||||
}
|
||||
if !hasDataStream {
|
||||
t.Error("Expected data stream messages")
|
||||
}
|
||||
|
||||
t.Logf("Multiple operations test passed: %d total messages", len(messages))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,11 @@ import (
|
||||
authHandlers "api-service/internal/handlers/auth"
|
||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||
"api-service/internal/handlers/websocket"
|
||||
websocketHandlers "api-service/internal/handlers/websocket"
|
||||
"api-service/internal/middleware"
|
||||
services "api-service/internal/services/auth"
|
||||
websocketServices "api-service/internal/services/websocket"
|
||||
"api-service/pkg/logger"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -41,9 +39,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
// Initialize database service
|
||||
dbService := database.New(cfg)
|
||||
|
||||
// Initialize WebSocket handler with enhanced features
|
||||
websocketHandler := websocketHandlers.NewWebSocketHandler(cfg, dbService)
|
||||
|
||||
// =============================================================================
|
||||
// HEALTH CHECK & SYSTEM ROUTES
|
||||
// =============================================================================
|
||||
@@ -63,7 +58,7 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
c.JSON(200, gin.H{
|
||||
"service": "API Service v1.0.0",
|
||||
"websocket_active": true,
|
||||
"connected_clients": websocketHandler.GetConnectedClients(),
|
||||
"connected_clients": 0, // TODO: implement websocket handler
|
||||
"databases": dbService.ListDBs(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
@@ -84,11 +79,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
// WEBSOCKET TEST CLIENT
|
||||
// =============================================================================
|
||||
|
||||
// router.GET("/websocket-test", func(c *gin.Context) {
|
||||
// c.Header("Content-Type", "text/html")
|
||||
// c.String(http.StatusOK, getWebSocketTestHTML())
|
||||
// })
|
||||
|
||||
// =============================================================================
|
||||
// API v1 GROUP
|
||||
// =============================================================================
|
||||
@@ -116,559 +106,24 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
// WEBSOCKET ROUTES
|
||||
// =============================================================================
|
||||
|
||||
// Main WebSocket endpoint with enhanced features
|
||||
v1.GET("/ws", websocketHandler.HandleWebSocket)
|
||||
|
||||
// WebSocket management API
|
||||
wsAPI := router.Group("/api/websocket")
|
||||
{
|
||||
// =============================================================================
|
||||
// BASIC BROADCASTING
|
||||
// =============================================================================
|
||||
|
||||
wsAPI.POST("/broadcast", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Message interface{} `json:"message"`
|
||||
Database string `json:"database,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
websocketHandler.BroadcastMessage(req.Type, req.Message)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "broadcast sent",
|
||||
"clients_count": websocketHandler.GetConnectedClients(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
wsAPI.POST("/broadcast/room/:room", func(c *gin.Context) {
|
||||
room := c.Param("room")
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Message interface{} `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
websocketHandler.BroadcastToRoom(room, req.Type, req.Message)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "room broadcast sent",
|
||||
"room": room,
|
||||
"clients_count": websocketHandler.GetRoomClientCount(room), // Fix: gunakan GetRoomClientCount
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// ENHANCED CLIENT TARGETING
|
||||
// =============================================================================
|
||||
|
||||
wsAPI.POST("/send/:clientId", func(c *gin.Context) {
|
||||
clientID := c.Param("clientId")
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Message interface{} `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
websocketHandler.SendToClient(clientID, req.Type, req.Message)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "message sent",
|
||||
"client_id": clientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Send to client by static ID
|
||||
wsAPI.POST("/send/static/:staticId", func(c *gin.Context) {
|
||||
staticID := c.Param("staticId")
|
||||
logger.Infof("Sending message to static client: %s", staticID)
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Message interface{} `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
success := websocketHandler.SendToClientByStaticID(staticID, req.Type, req.Message)
|
||||
if success {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "message sent to static client",
|
||||
"static_id": staticID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
} else {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "static client not found",
|
||||
"static_id": staticID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Broadcast to all clients from specific IP
|
||||
wsAPI.POST("/broadcast/ip/:ipAddress", func(c *gin.Context) {
|
||||
ipAddress := c.Param("ipAddress")
|
||||
var req struct {
|
||||
Type string `json:"type"`
|
||||
Message interface{} `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
count := websocketHandler.BroadcastToIP(ipAddress, req.Type, req.Message)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ip broadcast sent",
|
||||
"ip_address": ipAddress,
|
||||
"clients_count": count,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// CLIENT INFORMATION & STATISTICS
|
||||
// =============================================================================
|
||||
|
||||
wsAPI.GET("/stats", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"connected_clients": websocketHandler.GetConnectedClients(),
|
||||
"databases": dbService.ListDBs(),
|
||||
"database_health": dbService.Health(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
wsAPI.GET("/stats/detailed", func(c *gin.Context) {
|
||||
stats := websocketHandler.GetDetailedStats()
|
||||
c.JSON(200, gin.H{
|
||||
"stats": stats,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
wsAPI.GET("/clients", func(c *gin.Context) {
|
||||
clients := websocketHandler.GetAllClients()
|
||||
c.JSON(200, gin.H{
|
||||
"clients": clients,
|
||||
"count": len(clients),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Fix: Perbaiki GetClientsByIP untuk menggunakan ClientInfo
|
||||
wsAPI.GET("/clients/by-ip/:ipAddress", func(c *gin.Context) {
|
||||
ipAddress := c.Param("ipAddress")
|
||||
client := websocketHandler.GetClientsByIP(ipAddress)
|
||||
if client == nil {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "client not found",
|
||||
"ip_address": ipAddress,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Use ClientInfo struct instead of direct field access
|
||||
clientInfo := websocketHandler.GetAllClients()
|
||||
var targetClientInfo *websocket.ClientInfo
|
||||
for i := range clientInfo {
|
||||
if clientInfo[i].ID == ipAddress {
|
||||
targetClientInfo = &clientInfo[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetClientInfo == nil {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "ipAddress not found",
|
||||
"client_id": ipAddress,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"client": map[string]interface{}{
|
||||
"id": targetClientInfo.ID,
|
||||
"static_id": targetClientInfo.StaticID,
|
||||
"ip_address": targetClientInfo.IPAddress,
|
||||
"user_id": targetClientInfo.UserID,
|
||||
"room": targetClientInfo.Room,
|
||||
"connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field
|
||||
"last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field
|
||||
},
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// Fix: Perbaiki GetClientByID response
|
||||
wsAPI.GET("/client/:clientId", func(c *gin.Context) {
|
||||
clientID := c.Param("clientId")
|
||||
client := websocketHandler.GetClientByID(clientID)
|
||||
|
||||
if client == nil {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "client not found",
|
||||
"client_id": clientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Use ClientInfo struct instead of direct field access
|
||||
clientInfo := websocketHandler.GetAllClients()
|
||||
var targetClientInfo *websocket.ClientInfo
|
||||
for i := range clientInfo {
|
||||
if clientInfo[i].ID == clientID {
|
||||
targetClientInfo = &clientInfo[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetClientInfo == nil {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "client not found",
|
||||
"client_id": clientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"client": map[string]interface{}{
|
||||
"id": targetClientInfo.ID,
|
||||
"static_id": targetClientInfo.StaticID,
|
||||
"ip_address": targetClientInfo.IPAddress,
|
||||
"user_id": targetClientInfo.UserID,
|
||||
"room": targetClientInfo.Room,
|
||||
"connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field
|
||||
"last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field
|
||||
},
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Fix: Perbaiki GetClientByStaticID response
|
||||
wsAPI.GET("/client/static/:staticId", func(c *gin.Context) {
|
||||
staticID := c.Param("staticId")
|
||||
client := websocketHandler.GetClientByStaticID(staticID)
|
||||
|
||||
if client == nil {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "static client not found",
|
||||
"static_id": staticID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Use ClientInfo struct instead of direct field access
|
||||
clientInfo := websocketHandler.GetAllClients()
|
||||
var targetClientInfo *websocket.ClientInfo
|
||||
for i := range clientInfo {
|
||||
if clientInfo[i].StaticID == staticID {
|
||||
targetClientInfo = &clientInfo[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetClientInfo == nil {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "static client not found",
|
||||
"static_id": staticID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"client": map[string]interface{}{
|
||||
"id": targetClientInfo.ID,
|
||||
"static_id": targetClientInfo.StaticID,
|
||||
"ip_address": targetClientInfo.IPAddress,
|
||||
"user_id": targetClientInfo.UserID,
|
||||
"room": targetClientInfo.Room,
|
||||
"connected_at": targetClientInfo.ConnectedAt.Unix(), // Fixed: use exported field
|
||||
"last_ping": targetClientInfo.LastPing.Unix(), // Fixed: use exported field
|
||||
},
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// ACTIVE CLIENTS & CLEANUP
|
||||
// =============================================================================
|
||||
|
||||
// Tambahkan endpoint untuk active clients
|
||||
wsAPI.GET("/clients/active", func(c *gin.Context) {
|
||||
// Default: clients active dalam 5 menit terakhir
|
||||
minutes := c.DefaultQuery("minutes", "5")
|
||||
minutesInt, err := strconv.Atoi(minutes)
|
||||
if err != nil {
|
||||
minutesInt = 5
|
||||
}
|
||||
|
||||
activeClients := websocketHandler.GetActiveClients(time.Duration(minutesInt) * time.Minute)
|
||||
c.JSON(200, gin.H{
|
||||
"active_clients": activeClients,
|
||||
"count": len(activeClients),
|
||||
"threshold_minutes": minutesInt,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Tambahkan endpoint untuk cleanup inactive clients
|
||||
wsAPI.POST("/cleanup/inactive", func(c *gin.Context) {
|
||||
var req struct {
|
||||
InactiveMinutes int `json:"inactive_minutes"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req.InactiveMinutes = 30 // Default 30 minutes
|
||||
}
|
||||
|
||||
if req.InactiveMinutes <= 0 {
|
||||
req.InactiveMinutes = 30
|
||||
}
|
||||
|
||||
cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "cleanup completed",
|
||||
"cleaned_clients": cleanedCount,
|
||||
"inactive_minutes": req.InactiveMinutes,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// DATABASE NOTIFICATIONS
|
||||
// =============================================================================
|
||||
|
||||
wsAPI.POST("/notify/:database/:channel", func(c *gin.Context) {
|
||||
database := c.Param("database")
|
||||
channel := c.Param("channel")
|
||||
|
||||
var req struct {
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
payloadJSON, _ := json.Marshal(req.Payload)
|
||||
err := dbService.NotifyChange(database, channel, string(payloadJSON))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"error": err.Error(),
|
||||
"database": database,
|
||||
"channel": channel,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": "notification sent",
|
||||
"database": database,
|
||||
"channel": channel,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Test database notification
|
||||
wsAPI.POST("/test-notification", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Database string `json:"database"`
|
||||
Channel string `json:"channel"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Default values
|
||||
if req.Database == "" {
|
||||
req.Database = "default"
|
||||
}
|
||||
if req.Channel == "" {
|
||||
req.Channel = "system_changes"
|
||||
}
|
||||
if req.Message == "" {
|
||||
req.Message = "Test notification from API"
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"operation": "API_TEST",
|
||||
"table": "manual_test",
|
||||
"data": map[string]interface{}{
|
||||
"message": req.Message,
|
||||
"test_data": req.Data,
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
payloadJSON, _ := json.Marshal(payload)
|
||||
err := dbService.NotifyChange(req.Database, req.Channel, string(payloadJSON))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": "test notification sent",
|
||||
"database": req.Database,
|
||||
"channel": req.Channel,
|
||||
"payload": payload,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// ROOM MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
wsAPI.GET("/rooms", func(c *gin.Context) {
|
||||
rooms := websocketHandler.GetAllRooms()
|
||||
c.JSON(200, gin.H{
|
||||
"rooms": rooms,
|
||||
"count": len(rooms),
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
wsAPI.GET("/room/:room/clients", func(c *gin.Context) {
|
||||
room := c.Param("room")
|
||||
clientCount := websocketHandler.GetRoomClientCount(room)
|
||||
|
||||
// Get detailed room info
|
||||
allRooms := websocketHandler.GetAllRooms()
|
||||
roomClients := allRooms[room]
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"room": room,
|
||||
"client_count": clientCount,
|
||||
"clients": roomClients,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// MONITORING & DEBUGGING
|
||||
// =============================================================================
|
||||
|
||||
wsAPI.GET("/monitor", func(c *gin.Context) {
|
||||
monitor := websocketHandler.GetMonitoringData()
|
||||
c.JSON(200, monitor)
|
||||
})
|
||||
|
||||
wsAPI.POST("/ping-client/:clientId", func(c *gin.Context) {
|
||||
clientID := c.Param("clientId")
|
||||
websocketHandler.SendToClient(clientID, "server_ping", map[string]interface{}{
|
||||
"message": "Ping from server",
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ping sent",
|
||||
"client_id": clientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Disconnect specific client
|
||||
wsAPI.POST("/disconnect/:clientId", func(c *gin.Context) {
|
||||
clientID := c.Param("clientId")
|
||||
success := websocketHandler.DisconnectClient(clientID)
|
||||
if success {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "client disconnected",
|
||||
"client_id": clientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
} else {
|
||||
c.JSON(404, gin.H{
|
||||
"error": "client not found",
|
||||
"client_id": clientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// BULK OPERATIONS
|
||||
// =============================================================================
|
||||
|
||||
// Broadcast to multiple clients
|
||||
wsAPI.POST("/broadcast/bulk", func(c *gin.Context) {
|
||||
var req struct {
|
||||
ClientIDs []string `json:"client_ids"`
|
||||
Type string `json:"type"`
|
||||
Message interface{} `json:"message"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
for _, clientID := range req.ClientIDs {
|
||||
websocketHandler.SendToClient(clientID, req.Type, req.Message)
|
||||
successCount++
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": "bulk broadcast sent",
|
||||
"total_clients": len(req.ClientIDs),
|
||||
"success_count": successCount,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
// Disconnect multiple clients
|
||||
wsAPI.POST("/disconnect/bulk", func(c *gin.Context) {
|
||||
var req struct {
|
||||
ClientIDs []string `json:"client_ids"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
for _, clientID := range req.ClientIDs {
|
||||
if websocketHandler.DisconnectClient(clientID) {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": "bulk disconnect completed",
|
||||
"total_clients": len(req.ClientIDs),
|
||||
"success_count": successCount,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
}
|
||||
// WebSocket handler will be initialized after websocketHub is defined
|
||||
|
||||
// =============================================================================
|
||||
// PUBLISHED ROUTES
|
||||
// =============================================================================
|
||||
|
||||
// Initialize WebSocket hub with database service
|
||||
websocketHub := websocketServices.NewHub(cfg.WebSocket)
|
||||
go websocketHub.Run() // Start WebSocket hub in background
|
||||
|
||||
// Initialize WebSocket handler
|
||||
websocketHandler := websocketHandlers.NewWebSocketHandler(websocketHub, cfg.WebSocket)
|
||||
|
||||
// WebSocket routes
|
||||
v1.GET("/ws", websocketHandler.HandleWebSocket)
|
||||
v1.GET("/ws/test", websocketHandler.TestWebSocketConnection)
|
||||
v1.GET("/ws/stats", websocketHandler.GetWebSocketStats)
|
||||
|
||||
// Retribusi endpoints with WebSocket notifications
|
||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
retribusiGroup := v1.Group("/retribusi")
|
||||
@@ -684,91 +139,36 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
|
||||
// Trigger WebSocket notification after successful creation
|
||||
if c.Writer.Status() == 200 || c.Writer.Status() == 201 {
|
||||
websocketHandler.BroadcastMessage("retribusi_created", map[string]interface{}{
|
||||
"message": "New retribusi record created",
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
// Notify database change via WebSocket
|
||||
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
|
||||
// fmt.Sprintf(`{"action": "created", "timestamp": "%s"}`, time.Now().Format(time.RFC3339)))
|
||||
}
|
||||
})
|
||||
|
||||
retribusiGroup.PUT("/id/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// id := c.Param("id")
|
||||
retribusiHandler.UpdateRetribusi(c)
|
||||
|
||||
// Trigger WebSocket notification after successful update
|
||||
if c.Writer.Status() == 200 {
|
||||
websocketHandler.BroadcastMessage("retribusi_updated", map[string]interface{}{
|
||||
"message": "Retribusi record updated",
|
||||
"id": id,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
// Notify database change via WebSocket
|
||||
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
|
||||
// fmt.Sprintf(`{"action": "updated", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
|
||||
}
|
||||
})
|
||||
|
||||
retribusiGroup.DELETE("/id/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// id := c.Param("id")
|
||||
retribusiHandler.DeleteRetribusi(c)
|
||||
|
||||
// Trigger WebSocket notification after successful deletion
|
||||
if c.Writer.Status() == 200 {
|
||||
websocketHandler.BroadcastMessage("retribusi_deleted", map[string]interface{}{
|
||||
"message": "Retribusi record deleted",
|
||||
"id": id,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
// Notify database change via WebSocket
|
||||
// websocketHub.NotifyDatabaseChange("postgres_satudata", "retribusi_changes",
|
||||
// fmt.Sprintf(`{"action": "deleted", "id": "%s", "timestamp": "%s"}`, id, time.Now().Format(time.RFC3339)))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROTECTED ROUTES (Authentication Required)
|
||||
// =============================================================================
|
||||
|
||||
protected := v1.Group("/")
|
||||
protected.Use(middleware.ConfigurableAuthMiddleware(cfg))
|
||||
|
||||
// Protected WebSocket management (optional)
|
||||
protectedWS := protected.Group("/ws-admin")
|
||||
{
|
||||
protectedWS.GET("/stats", func(c *gin.Context) {
|
||||
detailedStats := websocketHandler.GetDetailedStats()
|
||||
c.JSON(200, gin.H{
|
||||
"admin_stats": detailedStats,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
protectedWS.POST("/force-disconnect/:clientId", func(c *gin.Context) {
|
||||
clientID := c.Param("clientId")
|
||||
success := websocketHandler.DisconnectClient(clientID)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "force disconnect attempted",
|
||||
"client_id": clientID,
|
||||
"success": success,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
|
||||
protectedWS.POST("/cleanup/force", func(c *gin.Context) {
|
||||
var req struct {
|
||||
InactiveMinutes int `json:"inactive_minutes"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
req.InactiveMinutes = 10
|
||||
req.Force = false
|
||||
}
|
||||
|
||||
cleanedCount := websocketHandler.CleanupInactiveClients(time.Duration(req.InactiveMinutes) * time.Minute)
|
||||
c.JSON(200, gin.H{
|
||||
"status": "admin cleanup completed",
|
||||
"cleaned_clients": cleanedCount,
|
||||
"inactive_minutes": req.InactiveMinutes,
|
||||
"force": req.Force,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
195
internal/services/websocket/broadcaster.go
Normal file
195
internal/services/websocket/broadcaster.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Broadcaster mengelola broadcasting pesan
|
||||
type Broadcaster struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
// NewBroadcaster membuat broadcaster baru
|
||||
func NewBroadcaster(hub *Hub) *Broadcaster {
|
||||
return &Broadcaster{hub: hub}
|
||||
}
|
||||
|
||||
// StartServerBroadcasters memulai broadcaster server
|
||||
func (b *Broadcaster) StartServerBroadcasters() {
|
||||
// Heartbeat broadcaster
|
||||
go b.startHeartbeatBroadcaster()
|
||||
|
||||
// System notification broadcaster
|
||||
go b.startSystemNotificationBroadcaster()
|
||||
|
||||
// Data stream broadcaster
|
||||
go b.startDataStreamBroadcaster()
|
||||
}
|
||||
|
||||
// startHeartbeatBroadcaster memulai broadcaster heartbeat
|
||||
func (b *Broadcaster) startHeartbeatBroadcaster() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.hub.mu.RLock()
|
||||
connectedClients := len(b.hub.clients)
|
||||
uniqueIPs := len(b.hub.clientsByIP)
|
||||
staticClients := len(b.hub.clientsByStatic)
|
||||
b.hub.mu.RUnlock()
|
||||
|
||||
b.BroadcastMessage("server_heartbeat", map[string]interface{}{
|
||||
"message": "Server heartbeat",
|
||||
"connected_clients": connectedClients,
|
||||
"unique_ips": uniqueIPs,
|
||||
"static_clients": staticClients,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"server_id": b.hub.config.ServerID,
|
||||
})
|
||||
case <-b.hub.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startSystemNotificationBroadcaster memulai broadcaster notifikasi sistem
|
||||
func (b *Broadcaster) startSystemNotificationBroadcaster() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
dbHealth := b.hub.dbService.Health()
|
||||
b.BroadcastMessage("system_status", map[string]interface{}{
|
||||
"type": "system_notification",
|
||||
"database": dbHealth,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"uptime": time.Since(b.hub.startTime).String(),
|
||||
})
|
||||
case <-b.hub.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startDataStreamBroadcaster memulai broadcaster aliran data
|
||||
func (b *Broadcaster) startDataStreamBroadcaster() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
counter := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
counter++
|
||||
b.BroadcastMessage("data_stream", map[string]interface{}{
|
||||
"id": counter,
|
||||
"value": counter * 10,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"type": "real_time_data",
|
||||
})
|
||||
case <-b.hub.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastMessage mengirim pesan ke semua klien
|
||||
func (b *Broadcaster) BroadcastMessage(messageType string, data interface{}) {
|
||||
msg := NewWebSocketMessage(MessageType(messageType), data, "", "")
|
||||
|
||||
select {
|
||||
case b.hub.messageQueue <- msg:
|
||||
default:
|
||||
// Antrian penuh, abaikan pesan
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastToRoom mengirim pesan ke ruangan tertentu
|
||||
func (b *Broadcaster) BroadcastToRoom(room string, messageType string, data interface{}) {
|
||||
msg := NewWebSocketMessage(
|
||||
MessageType(messageType),
|
||||
map[string]interface{}{
|
||||
"room": room,
|
||||
"data": data,
|
||||
},
|
||||
"",
|
||||
room,
|
||||
)
|
||||
|
||||
select {
|
||||
case b.hub.messageQueue <- msg:
|
||||
default:
|
||||
// Antrian penuh, abaikan pesan
|
||||
}
|
||||
}
|
||||
|
||||
// SendToClient mengirim pesan ke klien tertentu
|
||||
func (b *Broadcaster) SendToClient(clientID string, messageType string, data interface{}) {
|
||||
msg := NewWebSocketMessage(MessageType(messageType), data, clientID, "")
|
||||
|
||||
select {
|
||||
case b.hub.messageQueue <- msg:
|
||||
default:
|
||||
// Antrian penuh, abaikan pesan
|
||||
}
|
||||
}
|
||||
|
||||
// SendToClientByStaticID mengirim pesan ke klien berdasarkan ID statis
|
||||
func (b *Broadcaster) SendToClientByStaticID(staticID string, messageType string, data interface{}) bool {
|
||||
b.hub.mu.RLock()
|
||||
client, exists := b.hub.clientsByStatic[staticID]
|
||||
b.hub.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
b.SendToClient(client.ID, messageType, data)
|
||||
return true
|
||||
}
|
||||
|
||||
// BroadcastToIP mengirim pesan ke semua klien dari IP tertentu
|
||||
func (b *Broadcaster) BroadcastToIP(ipAddress string, messageType string, data interface{}) int {
|
||||
b.hub.mu.RLock()
|
||||
clients := b.hub.clientsByIP[ipAddress]
|
||||
b.hub.mu.RUnlock()
|
||||
|
||||
count := 0
|
||||
for _, client := range clients {
|
||||
b.SendToClient(client.ID, messageType, data)
|
||||
count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
// BroadcastClientToClient mengirim pesan dari satu klien ke klien lain
|
||||
func (b *Broadcaster) BroadcastClientToClient(fromClientID, toClientID string, messageType string, data interface{}) {
|
||||
message := NewWebSocketMessage(MessageType(messageType), map[string]interface{}{
|
||||
"from_client_id": fromClientID,
|
||||
"data": data,
|
||||
}, toClientID, "")
|
||||
b.hub.broadcast <- message
|
||||
}
|
||||
|
||||
// BroadcastServerToClient mengirim pesan dari server ke klien tertentu
|
||||
func (b *Broadcaster) BroadcastServerToClient(clientID string, messageType string, data interface{}) {
|
||||
message := NewWebSocketMessage(MessageType(messageType), data, clientID, "")
|
||||
b.hub.broadcast <- message
|
||||
}
|
||||
|
||||
// BroadcastServerToRoom mengirim pesan dari server ke ruangan tertentu
|
||||
func (b *Broadcaster) BroadcastServerToRoom(room string, messageType string, data interface{}) {
|
||||
message := NewWebSocketMessage(MessageType(messageType), data, "", room)
|
||||
b.hub.broadcast <- message
|
||||
}
|
||||
|
||||
// BroadcastServerToAll mengirim pesan dari server ke semua klien
|
||||
func (b *Broadcaster) BroadcastServerToAll(messageType string, data interface{}) {
|
||||
message := NewWebSocketMessage(MessageType(messageType), data, "", "")
|
||||
b.hub.broadcast <- message
|
||||
}
|
||||
728
internal/services/websocket/client.go
Normal file
728
internal/services/websocket/client.go
Normal file
@@ -0,0 +1,728 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Client mewakili koneksi klien WebSocket
|
||||
type Client struct {
|
||||
// Identifikasi
|
||||
ID string
|
||||
StaticID string
|
||||
UserID string
|
||||
Room string
|
||||
|
||||
// Koneksi
|
||||
IPAddress string
|
||||
Conn *websocket.Conn
|
||||
Send chan WebSocketMessage
|
||||
|
||||
// Hub
|
||||
Hub *Hub
|
||||
|
||||
// Context
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// Status
|
||||
lastPing time.Time
|
||||
lastPong time.Time
|
||||
connectedAt time.Time
|
||||
isActive bool
|
||||
|
||||
// Sinkronisasi
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewClient membuat klien baru
|
||||
func NewClient(
|
||||
id, staticID, userID, room, ipAddress string,
|
||||
conn *websocket.Conn,
|
||||
hub *Hub,
|
||||
config config.WebSocketConfig,
|
||||
) *Client {
|
||||
ctx, cancel := context.WithCancel(hub.ctx)
|
||||
|
||||
return &Client{
|
||||
ID: id,
|
||||
StaticID: staticID,
|
||||
UserID: userID,
|
||||
Room: room,
|
||||
IPAddress: ipAddress,
|
||||
Conn: conn,
|
||||
Send: make(chan WebSocketMessage, config.ChannelBufferSize),
|
||||
Hub: hub,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
lastPing: time.Now(),
|
||||
connectedAt: time.Now(),
|
||||
isActive: true,
|
||||
}
|
||||
}
|
||||
|
||||
// readPup menangani pembacaan pesan dari klien
|
||||
func (c *Client) readPump() {
|
||||
defer func() {
|
||||
c.Hub.unregister <- c
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
// Konfigurasi koneksi
|
||||
c.Conn.SetReadLimit(int64(c.Hub.config.MaxMessageSize))
|
||||
c.resetReadDeadline()
|
||||
|
||||
// Setup ping/pong handlers
|
||||
c.Conn.SetPingHandler(func(message string) error {
|
||||
c.resetReadDeadline()
|
||||
return c.Conn.WriteControl(websocket.PongMessage, []byte(message), time.Now().Add(c.Hub.config.WriteTimeout))
|
||||
})
|
||||
|
||||
c.Conn.SetPongHandler(func(message string) error {
|
||||
c.mu.Lock()
|
||||
c.lastPong = time.Now()
|
||||
c.isActive = true
|
||||
c.mu.Unlock()
|
||||
c.resetReadDeadline()
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
_, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err,
|
||||
websocket.CloseGoingAway,
|
||||
websocket.CloseAbnormalClosure,
|
||||
websocket.CloseNormalClosure) {
|
||||
c.Hub.errorCount++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Reset deadline setiap kali ada pesan masuk
|
||||
c.resetReadDeadline()
|
||||
c.updateLastActivity()
|
||||
|
||||
// Parse pesan
|
||||
var msg WebSocketMessage
|
||||
if err := json.Unmarshal(message, &msg); err != nil {
|
||||
c.sendErrorResponse("Invalid message format", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Set metadata pesan
|
||||
msg.Timestamp = time.Now()
|
||||
msg.ClientID = c.ID
|
||||
if msg.MessageID == "" {
|
||||
msg.MessageID = uuid.New().String()
|
||||
}
|
||||
|
||||
// Proses pesan menggunakan registry
|
||||
c.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writePump menangani pengiriman pesan ke klien
|
||||
func (c *Client) writePump() {
|
||||
ticker := time.NewTicker(c.Hub.config.PingInterval)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-c.Send:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
|
||||
if !ok {
|
||||
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.Conn.WriteJSON(message); err != nil {
|
||||
c.Hub.errorCount++
|
||||
return
|
||||
}
|
||||
|
||||
case <-ticker.C:
|
||||
// Kirim ping dan periksa respons
|
||||
if err := c.sendPing(); err != nil {
|
||||
c.Hub.errorCount++
|
||||
return
|
||||
}
|
||||
|
||||
// Periksa timeout pong
|
||||
if c.isPongTimeout() {
|
||||
return
|
||||
}
|
||||
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleMessage memproses pesan masuk
|
||||
func (c *Client) handleMessage(msg WebSocketMessage) {
|
||||
// Dapatkan handler dari registry
|
||||
handler, exists := c.Hub.messageRegistry.GetHandler(msg.Type)
|
||||
if !exists {
|
||||
// Handler tidak ditemukan, gunakan handler default
|
||||
c.handleDefaultMessage(msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Jalankan handler
|
||||
if err := handler.HandleMessage(c, msg); err != nil {
|
||||
c.sendErrorResponse("Error handling message", err.Error())
|
||||
c.Hub.errorCount++
|
||||
}
|
||||
}
|
||||
|
||||
// handleDefaultMessage menangani pesan tanpa handler khusus
|
||||
func (c *Client) handleDefaultMessage(msg WebSocketMessage) {
|
||||
switch msg.Type {
|
||||
case "broadcast":
|
||||
c.Hub.broadcast <- msg
|
||||
c.sendDirectResponse("broadcast_sent", "Message broadcasted to all clients")
|
||||
|
||||
case "direct_message":
|
||||
c.handleDirectMessage(msg)
|
||||
|
||||
case "ping":
|
||||
c.handlePing(msg)
|
||||
|
||||
case "pong":
|
||||
// Pong sudah ditangani di level koneksi
|
||||
break
|
||||
|
||||
case "heartbeat":
|
||||
c.handleHeartbeat(msg)
|
||||
|
||||
case "connection_test":
|
||||
c.handleConnectionTest(msg)
|
||||
|
||||
case "get_online_users":
|
||||
c.sendOnlineUsers()
|
||||
|
||||
case "join_room":
|
||||
c.handleJoinRoom(msg)
|
||||
|
||||
case "leave_room":
|
||||
c.handleLeaveRoom(msg)
|
||||
|
||||
case "get_room_info":
|
||||
c.handleGetRoomInfo(msg)
|
||||
|
||||
case "database_query", "db_query":
|
||||
c.handleDatabaseQuery(msg)
|
||||
|
||||
case "db_insert":
|
||||
c.handleDatabaseInsert(msg)
|
||||
|
||||
case "db_custom_query":
|
||||
c.handleDatabaseCustomQuery(msg)
|
||||
|
||||
case "get_stats":
|
||||
c.handleGetStats(msg)
|
||||
|
||||
case "get_server_stats":
|
||||
c.handleGetServerStats(msg)
|
||||
|
||||
case "get_system_health":
|
||||
c.handleGetSystemHealth(msg)
|
||||
|
||||
case "admin_kick_client":
|
||||
c.handleAdminKickClient(msg)
|
||||
|
||||
case "admin_kill_server":
|
||||
c.handleAdminKillServer(msg)
|
||||
|
||||
case "admin_clear_logs":
|
||||
c.handleAdminClearLogs(msg)
|
||||
|
||||
default:
|
||||
c.sendDirectResponse("message_received", fmt.Sprintf("Message received: %v", msg.Data))
|
||||
c.Hub.broadcast <- msg
|
||||
}
|
||||
}
|
||||
|
||||
// Metode helper lainnya...
|
||||
func (c *Client) resetReadDeadline() {
|
||||
c.Conn.SetReadDeadline(time.Now().Add(c.Hub.config.ReadTimeout))
|
||||
}
|
||||
|
||||
func (c *Client) updateLastActivity() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.lastPing = time.Now()
|
||||
c.isActive = true
|
||||
}
|
||||
|
||||
func (c *Client) sendPing() error {
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
|
||||
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.lastPing = time.Now()
|
||||
c.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) isPongTimeout() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
lastActivity := c.lastPong
|
||||
if lastActivity.IsZero() {
|
||||
lastActivity = c.lastPing
|
||||
}
|
||||
|
||||
return time.Since(lastActivity) > c.Hub.config.PongTimeout
|
||||
}
|
||||
|
||||
func (c *Client) isClientActive() bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.isActive && time.Since(c.lastPing) < c.Hub.config.PongTimeout
|
||||
}
|
||||
|
||||
func (c *Client) sendDirectResponse(messageType string, data interface{}) {
|
||||
response := NewWebSocketMessage(MessageType(messageType), data, c.ID, c.Room)
|
||||
|
||||
select {
|
||||
case c.Send <- response:
|
||||
default:
|
||||
// Channel penuh, abaikan pesan
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) sendErrorResponse(error, details string) {
|
||||
c.sendDirectResponse("error", map[string]interface{}{
|
||||
"error": error,
|
||||
"details": details,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) gracefulClose() {
|
||||
c.mu.Lock()
|
||||
c.isActive = false
|
||||
c.mu.Unlock()
|
||||
|
||||
// Kirim pesan close
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(c.Hub.config.WriteTimeout))
|
||||
c.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||||
|
||||
// Batalkan context
|
||||
c.cancel()
|
||||
}
|
||||
|
||||
// ReadPump mengekspos metode readPump untuk handler
|
||||
func (c *Client) ReadPump() {
|
||||
c.readPump()
|
||||
}
|
||||
|
||||
// WritePump mengekspos metode writePump untuk handler
|
||||
func (c *Client) WritePump() {
|
||||
c.writePump()
|
||||
}
|
||||
|
||||
// handleDirectMessage menangani pesan direct message
|
||||
func (c *Client) handleDirectMessage(msg WebSocketMessage) {
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid direct message format", "Data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
targetClientID, ok := data["target_client_id"].(string)
|
||||
if !ok || targetClientID == "" {
|
||||
c.sendErrorResponse("Invalid target client ID", "target_client_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
message, ok := data["message"].(string)
|
||||
if !ok || message == "" {
|
||||
c.sendErrorResponse("Invalid message content", "message is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Buat pesan direct message
|
||||
directMsg := NewWebSocketMessage(DirectMessage, map[string]interface{}{
|
||||
"from_client_id": c.ID,
|
||||
"from_user_id": c.UserID,
|
||||
"message": message,
|
||||
"timestamp": msg.Timestamp,
|
||||
}, targetClientID, "")
|
||||
|
||||
// Kirim ke target client
|
||||
select {
|
||||
case c.Hub.broadcast <- directMsg:
|
||||
c.sendDirectResponse("direct_message_sent", map[string]interface{}{
|
||||
"target_client_id": targetClientID,
|
||||
"message": message,
|
||||
})
|
||||
default:
|
||||
c.sendErrorResponse("Failed to send direct message", "Message queue is full")
|
||||
}
|
||||
}
|
||||
|
||||
// handlePing menangani ping message
|
||||
func (c *Client) handlePing(msg WebSocketMessage) {
|
||||
// Kirim pong response
|
||||
pongMsg := NewWebSocketMessage(PongMessage, map[string]interface{}{
|
||||
"timestamp": msg.Timestamp.Unix(),
|
||||
"client_id": c.ID,
|
||||
}, c.ID, "")
|
||||
|
||||
select {
|
||||
case c.Send <- pongMsg:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
}
|
||||
|
||||
// handleHeartbeat menangani heartbeat message
|
||||
func (c *Client) handleHeartbeat(msg WebSocketMessage) {
|
||||
// Kirim heartbeat acknowledgment
|
||||
heartbeatAck := NewWebSocketMessage(MessageType("heartbeat_ack"), map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"client_uptime": time.Since(c.connectedAt).Seconds(),
|
||||
"server_uptime": time.Since(c.Hub.startTime).Seconds(),
|
||||
"client_id": c.ID,
|
||||
}, c.ID, "")
|
||||
|
||||
select {
|
||||
case c.Send <- heartbeatAck:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnectionTest menangani connection test
|
||||
func (c *Client) handleConnectionTest(msg WebSocketMessage) {
|
||||
// Kirim connection test result
|
||||
testResult := NewWebSocketMessage(MessageType("connection_test_result"), map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"client_id": c.ID,
|
||||
"connection_status": "healthy",
|
||||
"latency_ms": 0, // Could be calculated if ping timestamp is provided
|
||||
"uptime_seconds": time.Since(c.connectedAt).Seconds(),
|
||||
}, c.ID, "")
|
||||
|
||||
select {
|
||||
case c.Send <- testResult:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
}
|
||||
|
||||
// handleJoinRoom menangani join room
|
||||
func (c *Client) handleJoinRoom(msg WebSocketMessage) {
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid join room format", "Data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
roomName, ok := data["room"].(string)
|
||||
if !ok || roomName == "" {
|
||||
c.sendErrorResponse("Invalid room name", "room is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Update client room
|
||||
oldRoom := c.Room
|
||||
c.Room = roomName
|
||||
|
||||
// Update hub room mapping
|
||||
c.Hub.mu.Lock()
|
||||
if oldRoom != "" {
|
||||
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
|
||||
delete(roomClients, c)
|
||||
if len(roomClients) == 0 {
|
||||
delete(c.Hub.rooms, oldRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Hub.rooms[roomName] == nil {
|
||||
c.Hub.rooms[roomName] = make(map[*Client]bool)
|
||||
}
|
||||
c.Hub.rooms[roomName][c] = true
|
||||
c.Hub.mu.Unlock()
|
||||
|
||||
// Log activity
|
||||
c.Hub.logActivity("client_join_room", c.ID, fmt.Sprintf("Room: %s", roomName))
|
||||
|
||||
// Kirim response
|
||||
c.sendDirectResponse("room_joined", map[string]interface{}{
|
||||
"room": roomName,
|
||||
"previous_room": oldRoom,
|
||||
})
|
||||
}
|
||||
|
||||
// handleLeaveRoom menangani leave room
|
||||
func (c *Client) handleLeaveRoom(msg WebSocketMessage) {
|
||||
oldRoom := c.Room
|
||||
|
||||
if oldRoom == "" {
|
||||
c.sendErrorResponse("Not in any room", "Client is not currently in a room")
|
||||
return
|
||||
}
|
||||
|
||||
// Update hub room mapping
|
||||
c.Hub.mu.Lock()
|
||||
if roomClients, exists := c.Hub.rooms[oldRoom]; exists {
|
||||
delete(roomClients, c)
|
||||
if len(roomClients) == 0 {
|
||||
delete(c.Hub.rooms, oldRoom)
|
||||
}
|
||||
}
|
||||
c.Hub.mu.Unlock()
|
||||
|
||||
// Clear client room
|
||||
c.Room = ""
|
||||
|
||||
// Log activity
|
||||
c.Hub.logActivity("client_leave_room", c.ID, fmt.Sprintf("Room: %s", oldRoom))
|
||||
|
||||
// Kirim response
|
||||
c.sendDirectResponse("room_left", map[string]interface{}{
|
||||
"room": oldRoom,
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetRoomInfo menangani get room info
|
||||
func (c *Client) handleGetRoomInfo(msg WebSocketMessage) {
|
||||
c.Hub.mu.RLock()
|
||||
defer c.Hub.mu.RUnlock()
|
||||
|
||||
roomInfo := make(map[string]interface{})
|
||||
|
||||
// Info ruangan saat ini
|
||||
if c.Room != "" {
|
||||
if roomClients, exists := c.Hub.rooms[c.Room]; exists {
|
||||
clientIDs := make([]string, 0, len(roomClients))
|
||||
for client := range roomClients {
|
||||
clientIDs = append(clientIDs, client.ID)
|
||||
}
|
||||
roomInfo["current_room"] = map[string]interface{}{
|
||||
"name": c.Room,
|
||||
"client_count": len(roomClients),
|
||||
"clients": clientIDs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info semua ruangan
|
||||
allRooms := make(map[string]int)
|
||||
for roomName, clients := range c.Hub.rooms {
|
||||
allRooms[roomName] = len(clients)
|
||||
}
|
||||
roomInfo["all_rooms"] = allRooms
|
||||
roomInfo["total_rooms"] = len(c.Hub.rooms)
|
||||
|
||||
c.sendDirectResponse("room_info", roomInfo)
|
||||
}
|
||||
|
||||
// handleDatabaseInsert menangani database insert
|
||||
func (c *Client) handleDatabaseInsert(msg WebSocketMessage) {
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid database insert format", "Data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
table, ok := data["table"].(string)
|
||||
if !ok || table == "" {
|
||||
c.sendErrorResponse("Invalid table name", "table is required")
|
||||
return
|
||||
}
|
||||
|
||||
insertData, ok := data["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid insert data", "data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, just acknowledge the insert request
|
||||
// In a real implementation, you would perform the actual database insert
|
||||
c.sendDirectResponse("db_insert_result", map[string]interface{}{
|
||||
"table": table,
|
||||
"status": "acknowledged",
|
||||
"message": "Insert request received (not implemented)",
|
||||
"data": insertData, // Use the variable to avoid unused error
|
||||
})
|
||||
}
|
||||
|
||||
// handleDatabaseCustomQuery menangani custom database query
|
||||
func (c *Client) handleDatabaseCustomQuery(msg WebSocketMessage) {
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid database query format", "Data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
database, ok := data["database"].(string)
|
||||
if !ok || database == "" {
|
||||
database = "default"
|
||||
}
|
||||
|
||||
query, ok := data["query"].(string)
|
||||
if !ok || query == "" {
|
||||
c.sendErrorResponse("Invalid query", "query is required")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, just acknowledge the query request
|
||||
// In a real implementation, you would execute the query
|
||||
c.sendDirectResponse("db_query_result", map[string]interface{}{
|
||||
"database": database,
|
||||
"query": query,
|
||||
"status": "acknowledged",
|
||||
"message": "Query request received (not implemented)",
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetStats menangani get stats
|
||||
func (c *Client) handleGetStats(msg WebSocketMessage) {
|
||||
stats := c.Hub.GetStats()
|
||||
c.sendDirectResponse("stats", stats)
|
||||
}
|
||||
|
||||
// handleGetServerStats menangani get server stats
|
||||
func (c *Client) handleGetServerStats(msg WebSocketMessage) {
|
||||
// Create monitoring manager instance
|
||||
monitoringManager := NewMonitoringManager(c.Hub)
|
||||
detailedStats := monitoringManager.GetDetailedStats()
|
||||
c.sendDirectResponse("server_stats", detailedStats)
|
||||
}
|
||||
|
||||
// handleGetSystemHealth menangani get system health
|
||||
func (c *Client) handleGetSystemHealth(msg WebSocketMessage) {
|
||||
systemHealth := make(map[string]interface{})
|
||||
if c.Hub.dbService != nil {
|
||||
systemHealth["databases"] = c.Hub.dbService.Health()
|
||||
systemHealth["available_dbs"] = c.Hub.dbService.ListDBs()
|
||||
}
|
||||
systemHealth["websocket_status"] = "healthy"
|
||||
systemHealth["uptime_seconds"] = time.Since(c.Hub.startTime).Seconds()
|
||||
|
||||
c.sendDirectResponse("system_health", systemHealth)
|
||||
}
|
||||
|
||||
// handleAdminKickClient menangani admin kick client
|
||||
func (c *Client) handleAdminKickClient(msg WebSocketMessage) {
|
||||
// This should be protected by authentication, but for now just acknowledge
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid admin command format", "Data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
targetClientID, ok := data["client_id"].(string)
|
||||
if !ok || targetClientID == "" {
|
||||
c.sendErrorResponse("Invalid target client ID", "client_id is required")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, just acknowledge the kick request
|
||||
// In a real implementation, you would check admin permissions and kick the client
|
||||
c.sendDirectResponse("admin_command_result", map[string]interface{}{
|
||||
"command": "kick_client",
|
||||
"target_client_id": targetClientID,
|
||||
"status": "acknowledged",
|
||||
"message": "Kick request received (not implemented)",
|
||||
})
|
||||
}
|
||||
|
||||
// handleAdminKillServer menangani admin kill server
|
||||
func (c *Client) handleAdminKillServer(msg WebSocketMessage) {
|
||||
// This should be protected by authentication, but for now just acknowledge
|
||||
c.sendDirectResponse("admin_command_result", map[string]interface{}{
|
||||
"command": "kill_server",
|
||||
"status": "acknowledged",
|
||||
"message": "Kill server request received (not implemented - would require admin auth)",
|
||||
})
|
||||
}
|
||||
|
||||
// handleAdminClearLogs menangani admin clear logs
|
||||
func (c *Client) handleAdminClearLogs(msg WebSocketMessage) {
|
||||
// This should be protected by authentication, but for now just acknowledge
|
||||
c.sendDirectResponse("admin_command_result", map[string]interface{}{
|
||||
"command": "clear_logs",
|
||||
"status": "acknowledged",
|
||||
"message": "Clear logs request received (not implemented - would require admin auth)",
|
||||
})
|
||||
}
|
||||
|
||||
// Implementasi metode lainnya...
|
||||
func (c *Client) handleDatabaseQuery(msg WebSocketMessage) {
|
||||
// Implementasi yang sama seperti sebelumnya
|
||||
data, ok := msg.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
c.sendErrorResponse("Invalid database query format", "Data must be an object")
|
||||
return
|
||||
}
|
||||
|
||||
table, ok := data["table"].(string)
|
||||
if !ok || table == "" {
|
||||
c.sendErrorResponse("Invalid table name", "table is required")
|
||||
return
|
||||
}
|
||||
|
||||
// For now, just acknowledge the query request
|
||||
c.sendDirectResponse("db_query_result", map[string]interface{}{
|
||||
"table": table,
|
||||
"status": "acknowledged",
|
||||
"message": "Query request received (not implemented)",
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) sendOnlineUsers() {
|
||||
c.Hub.mu.RLock()
|
||||
defer c.Hub.mu.RUnlock()
|
||||
|
||||
users := make([]map[string]interface{}, 0, len(c.Hub.clients))
|
||||
for client := range c.Hub.clients {
|
||||
user := map[string]interface{}{
|
||||
"id": client.ID,
|
||||
"static_id": client.StaticID,
|
||||
"user_id": client.UserID,
|
||||
"room": client.Room,
|
||||
"ip_address": client.IPAddress,
|
||||
"connected_at": client.connectedAt,
|
||||
"last_ping": client.lastPing,
|
||||
"last_pong": client.lastPong,
|
||||
"is_active": client.isClientActive(),
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
response := NewWebSocketMessage(OnlineUsersMessage, map[string]interface{}{
|
||||
"users": users,
|
||||
"count": len(users),
|
||||
}, c.ID, "")
|
||||
|
||||
select {
|
||||
case c.Send <- response:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
}
|
||||
621
internal/services/websocket/handlers.go
Normal file
621
internal/services/websocket/handlers.go
Normal file
@@ -0,0 +1,621 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DatabaseHandler menangani operasi database
|
||||
type DatabaseHandler struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
func NewDatabaseHandler(hub *Hub) *DatabaseHandler {
|
||||
return &DatabaseHandler{hub: hub}
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) HandleMessage(client *Client, message WebSocketMessage) error {
|
||||
switch message.Type {
|
||||
case DatabaseInsertMessage:
|
||||
return h.handleDatabaseInsert(client, message)
|
||||
case DatabaseQueryMessage:
|
||||
return h.handleDatabaseQuery(client, message)
|
||||
case DatabaseCustomQueryMessage:
|
||||
return h.handleDatabaseCustomQuery(client, message)
|
||||
default:
|
||||
return fmt.Errorf("unsupported database message type: %s", message.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) MessageType() MessageType {
|
||||
return DatabaseInsertMessage // Primary type for registration
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) handleDatabaseInsert(client *Client, message WebSocketMessage) error {
|
||||
data, ok := message.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
client.sendErrorResponse("Invalid database insert format", "Data must be an object")
|
||||
return nil
|
||||
}
|
||||
|
||||
table, ok := data["table"].(string)
|
||||
if !ok || table == "" {
|
||||
client.sendErrorResponse("Invalid table name", "table is required")
|
||||
return nil
|
||||
}
|
||||
|
||||
insertData, ok := data["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
client.sendErrorResponse("Invalid insert data", "data must be an object")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform actual database insert
|
||||
if h.hub.dbService != nil {
|
||||
// Get database connection
|
||||
db, err := h.hub.GetDatabaseConnection("postgres_satudata")
|
||||
if err != nil {
|
||||
client.sendErrorResponse("Database connection error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build insert query
|
||||
columns := make([]string, 0, len(insertData))
|
||||
values := make([]interface{}, 0, len(insertData))
|
||||
placeholders := make([]string, 0, len(insertData))
|
||||
|
||||
i := 1
|
||||
for col, val := range insertData {
|
||||
columns = append(columns, col)
|
||||
values = append(values, val)
|
||||
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
|
||||
i++
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||
table,
|
||||
strings.Join(columns, ", "),
|
||||
strings.Join(placeholders, ", "))
|
||||
|
||||
// Execute insert
|
||||
ctx, cancel := context.WithTimeout(client.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
result, err := db.ExecContext(ctx, query, values...)
|
||||
if err != nil {
|
||||
client.sendErrorResponse("Database insert error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
client.sendDirectResponse("db_insert_result", map[string]interface{}{
|
||||
"table": table,
|
||||
"status": "success",
|
||||
"rows_affected": rowsAffected,
|
||||
"message": "Data inserted successfully",
|
||||
})
|
||||
} else {
|
||||
client.sendErrorResponse("Database service not available", "Database service is not initialized")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) handleDatabaseQuery(client *Client, message WebSocketMessage) error {
|
||||
data, ok := message.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
client.sendErrorResponse("Invalid database query format", "Data must be an object")
|
||||
return nil
|
||||
}
|
||||
|
||||
table, ok := data["table"].(string)
|
||||
if !ok || table == "" {
|
||||
client.sendErrorResponse("Invalid table name", "table is required")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute query
|
||||
results, err := h.hub.ExecuteDatabaseQuery("postgres_satudata", fmt.Sprintf("SELECT * FROM %s LIMIT 100", table))
|
||||
if err != nil {
|
||||
client.sendErrorResponse("Database query error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
client.sendDirectResponse("db_query_result", map[string]interface{}{
|
||||
"table": table,
|
||||
"status": "success",
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DatabaseHandler) handleDatabaseCustomQuery(client *Client, message WebSocketMessage) error {
|
||||
data, ok := message.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
client.sendErrorResponse("Invalid database query format", "Data must be an object")
|
||||
return nil
|
||||
}
|
||||
|
||||
database, ok := data["database"].(string)
|
||||
if !ok || database == "" {
|
||||
database = "postgres_satudata"
|
||||
}
|
||||
|
||||
query, ok := data["query"].(string)
|
||||
if !ok || query == "" {
|
||||
client.sendErrorResponse("Invalid query", "query is required")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute custom query
|
||||
results, err := h.hub.ExecuteDatabaseQuery(database, query)
|
||||
if err != nil {
|
||||
client.sendErrorResponse("Database query error", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
client.sendDirectResponse("db_query_result", map[string]interface{}{
|
||||
"database": database,
|
||||
"query": query,
|
||||
"status": "success",
|
||||
"results": results,
|
||||
"count": len(results),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminHandler menangani operasi admin
|
||||
type AdminHandler struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
func NewAdminHandler(hub *Hub) *AdminHandler {
|
||||
return &AdminHandler{hub: hub}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) HandleMessage(client *Client, message WebSocketMessage) error {
|
||||
switch message.Type {
|
||||
case AdminKickClientMessage:
|
||||
return h.handleAdminKickClient(client, message)
|
||||
case AdminKillServerMessage:
|
||||
return h.handleAdminKillServer(client, message)
|
||||
case AdminClearLogsMessage:
|
||||
return h.handleAdminClearLogs(client, message)
|
||||
default:
|
||||
return fmt.Errorf("unsupported admin message type: %s", message.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) MessageType() MessageType {
|
||||
return AdminKickClientMessage // Primary type for registration
|
||||
}
|
||||
|
||||
func (h *AdminHandler) handleAdminKickClient(client *Client, message WebSocketMessage) error {
|
||||
data, ok := message.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
client.sendErrorResponse("Invalid admin command format", "Data must be an object")
|
||||
return nil
|
||||
}
|
||||
|
||||
targetClientID, ok := data["client_id"].(string)
|
||||
if !ok || targetClientID == "" {
|
||||
client.sendErrorResponse("Invalid target client ID", "client_id is required")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if target client exists
|
||||
h.hub.mu.RLock()
|
||||
targetClient, exists := h.hub.clientsByID[targetClientID]
|
||||
h.hub.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
client.sendErrorResponse("Client not found", fmt.Sprintf("Client %s not found", targetClientID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log activity
|
||||
h.hub.logActivity("admin_kick_client", client.ID, fmt.Sprintf("Kicked client: %s", targetClientID))
|
||||
|
||||
// Disconnect the target client
|
||||
targetClient.cancel()
|
||||
targetClient.Conn.Close()
|
||||
|
||||
client.sendDirectResponse("admin_command_result", map[string]interface{}{
|
||||
"command": "kick_client",
|
||||
"target_client_id": targetClientID,
|
||||
"status": "success",
|
||||
"message": "Client kicked successfully",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AdminHandler) handleAdminKillServer(client *Client, message WebSocketMessage) error {
|
||||
// Log activity
|
||||
h.hub.logActivity("admin_kill_server", client.ID, "Server kill signal received")
|
||||
|
||||
// For testing purposes, just acknowledge (don't actually kill server)
|
||||
client.sendDirectResponse("admin_command_result", map[string]interface{}{
|
||||
"command": "kill_server",
|
||||
"status": "acknowledged",
|
||||
"message": "Kill server signal received (test mode - server not actually killed)",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AdminHandler) handleAdminClearLogs(client *Client, message WebSocketMessage) error {
|
||||
// Clear activity logs
|
||||
h.hub.activityMu.Lock()
|
||||
h.hub.activityLog = make([]ActivityLog, 0, h.hub.config.ActivityLogSize)
|
||||
h.hub.activityMu.Unlock()
|
||||
|
||||
// Log the clear action
|
||||
h.hub.logActivity("admin_clear_logs", client.ID, "Activity logs cleared")
|
||||
|
||||
client.sendDirectResponse("admin_command_result", map[string]interface{}{
|
||||
"command": "clear_logs",
|
||||
"status": "success",
|
||||
"message": "Server logs cleared successfully",
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoomHandler menangani operasi ruangan
|
||||
type RoomHandler struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
func NewRoomHandler(hub *Hub) *RoomHandler {
|
||||
return &RoomHandler{hub: hub}
|
||||
}
|
||||
|
||||
func (h *RoomHandler) HandleMessage(client *Client, message WebSocketMessage) error {
|
||||
switch message.Type {
|
||||
case JoinRoomMessage:
|
||||
return h.handleJoinRoom(client, message)
|
||||
case LeaveRoomMessage:
|
||||
return h.handleLeaveRoom(client, message)
|
||||
case GetRoomInfoMessage:
|
||||
return h.handleGetRoomInfo(client, message)
|
||||
default:
|
||||
return fmt.Errorf("unsupported room message type: %s", message.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RoomHandler) MessageType() MessageType {
|
||||
return JoinRoomMessage // Primary type for registration
|
||||
}
|
||||
|
||||
func (h *RoomHandler) handleJoinRoom(client *Client, message WebSocketMessage) error {
|
||||
data, ok := message.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
client.sendErrorResponse("Invalid join room format", "Data must be an object")
|
||||
return nil
|
||||
}
|
||||
|
||||
roomName, ok := data["room"].(string)
|
||||
if !ok || roomName == "" {
|
||||
client.sendErrorResponse("Invalid room name", "room is required")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update client room
|
||||
oldRoom := client.Room
|
||||
client.Room = roomName
|
||||
|
||||
// Update hub room mapping
|
||||
h.hub.mu.Lock()
|
||||
if oldRoom != "" {
|
||||
if roomClients, exists := h.hub.rooms[oldRoom]; exists {
|
||||
delete(roomClients, client)
|
||||
if len(roomClients) == 0 {
|
||||
delete(h.hub.rooms, oldRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if h.hub.rooms[roomName] == nil {
|
||||
h.hub.rooms[roomName] = make(map[*Client]bool)
|
||||
}
|
||||
h.hub.rooms[roomName][client] = true
|
||||
h.hub.mu.Unlock()
|
||||
|
||||
// Log activity
|
||||
h.hub.logActivity("client_join_room", client.ID, fmt.Sprintf("Room: %s", roomName))
|
||||
|
||||
// Notify other clients in the room
|
||||
h.hub.broadcaster.BroadcastServerToRoom(roomName, "user_joined_room", map[string]interface{}{
|
||||
"client_id": client.ID,
|
||||
"user_id": client.UserID,
|
||||
"room": roomName,
|
||||
"joined_at": time.Now().Unix(),
|
||||
})
|
||||
|
||||
// Send response
|
||||
client.sendDirectResponse("room_joined", map[string]interface{}{
|
||||
"room": roomName,
|
||||
"previous_room": oldRoom,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RoomHandler) handleLeaveRoom(client *Client, message WebSocketMessage) error {
|
||||
oldRoom := client.Room
|
||||
|
||||
if oldRoom == "" {
|
||||
client.sendErrorResponse("Not in any room", "Client is not currently in a room")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update hub room mapping
|
||||
h.hub.mu.Lock()
|
||||
if roomClients, exists := h.hub.rooms[oldRoom]; exists {
|
||||
delete(roomClients, client)
|
||||
if len(roomClients) == 0 {
|
||||
delete(h.hub.rooms, oldRoom)
|
||||
}
|
||||
}
|
||||
h.hub.mu.Unlock()
|
||||
|
||||
// Clear client room
|
||||
client.Room = ""
|
||||
|
||||
// Log activity
|
||||
h.hub.logActivity("client_leave_room", client.ID, fmt.Sprintf("Room: %s", oldRoom))
|
||||
|
||||
// Notify other clients in the room
|
||||
h.hub.broadcaster.BroadcastServerToRoom(oldRoom, "user_left_room", map[string]interface{}{
|
||||
"client_id": client.ID,
|
||||
"user_id": client.UserID,
|
||||
"room": oldRoom,
|
||||
"left_at": time.Now().Unix(),
|
||||
})
|
||||
|
||||
// Send response
|
||||
client.sendDirectResponse("room_left", map[string]interface{}{
|
||||
"room": oldRoom,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RoomHandler) handleGetRoomInfo(client *Client, message WebSocketMessage) error {
|
||||
h.hub.mu.RLock()
|
||||
defer h.hub.mu.RUnlock()
|
||||
|
||||
roomInfo := make(map[string]interface{})
|
||||
|
||||
// Info ruangan saat ini
|
||||
if client.Room != "" {
|
||||
if roomClients, exists := h.hub.rooms[client.Room]; exists {
|
||||
clientIDs := make([]string, 0, len(roomClients))
|
||||
for client := range roomClients {
|
||||
clientIDs = append(clientIDs, client.ID)
|
||||
}
|
||||
roomInfo["current_room"] = map[string]interface{}{
|
||||
"name": client.Room,
|
||||
"client_count": len(roomClients),
|
||||
"clients": clientIDs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Info semua ruangan
|
||||
allRooms := make(map[string]int)
|
||||
for roomName, clients := range h.hub.rooms {
|
||||
allRooms[roomName] = len(clients)
|
||||
}
|
||||
roomInfo["all_rooms"] = allRooms
|
||||
roomInfo["total_rooms"] = len(h.hub.rooms)
|
||||
|
||||
client.sendDirectResponse("room_info", roomInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MonitoringHandler menangani operasi monitoring
|
||||
type MonitoringHandler struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
func NewMonitoringHandler(hub *Hub) *MonitoringHandler {
|
||||
return &MonitoringHandler{hub: hub}
|
||||
}
|
||||
|
||||
func (h *MonitoringHandler) HandleMessage(client *Client, message WebSocketMessage) error {
|
||||
switch message.Type {
|
||||
case GetStatsMessage:
|
||||
return h.handleGetStats(client, message)
|
||||
case GetServerStatsMessage:
|
||||
return h.handleGetServerStats(client, message)
|
||||
case GetSystemHealthMessage:
|
||||
return h.handleGetSystemHealth(client, message)
|
||||
default:
|
||||
return fmt.Errorf("unsupported monitoring message type: %s", message.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MonitoringHandler) MessageType() MessageType {
|
||||
return GetStatsMessage // Primary type for registration
|
||||
}
|
||||
|
||||
func (h *MonitoringHandler) handleGetStats(client *Client, message WebSocketMessage) error {
|
||||
stats := h.hub.GetStats()
|
||||
client.sendDirectResponse("stats", stats)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MonitoringHandler) handleGetServerStats(client *Client, message WebSocketMessage) error {
|
||||
// Create monitoring manager instance
|
||||
monitoringManager := NewMonitoringManager(h.hub)
|
||||
detailedStats := monitoringManager.GetDetailedStats()
|
||||
client.sendDirectResponse("server_stats", detailedStats)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MonitoringHandler) handleGetSystemHealth(client *Client, message WebSocketMessage) error {
|
||||
systemHealth := make(map[string]interface{})
|
||||
if h.hub.dbService != nil {
|
||||
systemHealth["databases"] = h.hub.dbService.Health()
|
||||
systemHealth["available_dbs"] = h.hub.dbService.ListDBs()
|
||||
}
|
||||
systemHealth["websocket_status"] = "healthy"
|
||||
systemHealth["uptime_seconds"] = time.Since(h.hub.startTime).Seconds()
|
||||
|
||||
client.sendDirectResponse("system_health", systemHealth)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectionHandler menangani operasi koneksi
|
||||
type ConnectionHandler struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
func NewConnectionHandler(hub *Hub) *ConnectionHandler {
|
||||
return &ConnectionHandler{hub: hub}
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) HandleMessage(client *Client, message WebSocketMessage) error {
|
||||
switch message.Type {
|
||||
case PingMessage:
|
||||
return h.handlePing(client, message)
|
||||
case PongMessage:
|
||||
return h.handlePong(client, message)
|
||||
case HeartbeatMessage:
|
||||
return h.handleHeartbeat(client, message)
|
||||
case ConnectionTestMessage:
|
||||
return h.handleConnectionTest(client, message)
|
||||
case OnlineUsersMessage:
|
||||
return h.handleGetOnlineUsers(client, message)
|
||||
default:
|
||||
return fmt.Errorf("unsupported connection message type: %s", message.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) MessageType() MessageType {
|
||||
return PingMessage // Primary type for registration
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) handlePing(client *Client, message WebSocketMessage) error {
|
||||
// Send pong response
|
||||
pongMsg := NewWebSocketMessage(PongMessage, map[string]interface{}{
|
||||
"timestamp": message.Timestamp.Unix(),
|
||||
"client_id": client.ID,
|
||||
}, client.ID, "")
|
||||
|
||||
select {
|
||||
case client.Send <- pongMsg:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) handlePong(client *Client, message WebSocketMessage) error {
|
||||
// Pong sudah ditangani di level koneksi, tapi kita bisa log aktivitas
|
||||
h.hub.logActivity("pong_received", client.ID, "Pong message received")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) handleHeartbeat(client *Client, message WebSocketMessage) error {
|
||||
// Send heartbeat acknowledgment
|
||||
heartbeatAck := NewWebSocketMessage(MessageType("heartbeat_ack"), map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"client_uptime": time.Since(client.connectedAt).Seconds(),
|
||||
"server_uptime": time.Since(h.hub.startTime).Seconds(),
|
||||
"client_id": client.ID,
|
||||
}, client.ID, "")
|
||||
|
||||
select {
|
||||
case client.Send <- heartbeatAck:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) handleConnectionTest(client *Client, message WebSocketMessage) error {
|
||||
// Send connection test result
|
||||
testResult := NewWebSocketMessage(MessageType("connection_test_result"), map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"client_id": client.ID,
|
||||
"connection_status": "healthy",
|
||||
"latency_ms": 0, // Could be calculated if ping timestamp is provided
|
||||
"uptime_seconds": time.Since(client.connectedAt).Seconds(),
|
||||
}, client.ID, "")
|
||||
|
||||
select {
|
||||
case client.Send <- testResult:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ConnectionHandler) handleGetOnlineUsers(client *Client, message WebSocketMessage) error {
|
||||
h.hub.mu.RLock()
|
||||
defer h.hub.mu.RUnlock()
|
||||
|
||||
users := make([]map[string]interface{}, 0, len(h.hub.clients))
|
||||
for client := range h.hub.clients {
|
||||
user := map[string]interface{}{
|
||||
"id": client.ID,
|
||||
"static_id": client.StaticID,
|
||||
"user_id": client.UserID,
|
||||
"room": client.Room,
|
||||
"ip_address": client.IPAddress,
|
||||
"connected_at": client.connectedAt,
|
||||
"last_ping": client.lastPing,
|
||||
"last_pong": client.lastPong,
|
||||
"is_active": client.isClientActive(),
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
response := NewWebSocketMessage(OnlineUsersMessage, map[string]interface{}{
|
||||
"users": users,
|
||||
"count": len(users),
|
||||
}, client.ID, "")
|
||||
|
||||
select {
|
||||
case client.Send <- response:
|
||||
default:
|
||||
// Channel penuh, abaikan
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupDefaultHandlers sets up all default message handlers
|
||||
func SetupDefaultHandlers(registry *MessageRegistry, hub *Hub) {
|
||||
// Register database handler
|
||||
dbHandler := NewDatabaseHandler(hub)
|
||||
registry.RegisterHandler(dbHandler)
|
||||
|
||||
// Register admin handler
|
||||
adminHandler := NewAdminHandler(hub)
|
||||
registry.RegisterHandler(adminHandler)
|
||||
|
||||
// Register room handler
|
||||
roomHandler := NewRoomHandler(hub)
|
||||
registry.RegisterHandler(roomHandler)
|
||||
|
||||
// Register monitoring handler
|
||||
monitoringHandler := NewMonitoringHandler(hub)
|
||||
registry.RegisterHandler(monitoringHandler)
|
||||
|
||||
// Register connection handler
|
||||
connectionHandler := NewConnectionHandler(hub)
|
||||
registry.RegisterHandler(connectionHandler)
|
||||
}
|
||||
444
internal/services/websocket/hub.go
Normal file
444
internal/services/websocket/hub.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Hub mengelola semua klien WebSocket
|
||||
type Hub struct {
|
||||
// Konfigurasi
|
||||
config config.WebSocketConfig
|
||||
|
||||
// Klien
|
||||
clients map[*Client]bool
|
||||
clientsByID map[string]*Client
|
||||
clientsByIP map[string][]*Client
|
||||
clientsByStatic map[string]*Client
|
||||
|
||||
// Ruangan
|
||||
rooms map[string]map[*Client]bool
|
||||
|
||||
// Channel komunikasi
|
||||
broadcast chan WebSocketMessage
|
||||
register chan *Client
|
||||
unregister chan *Client
|
||||
messageQueue chan WebSocketMessage
|
||||
|
||||
// Sinkronisasi
|
||||
mu sync.RWMutex
|
||||
|
||||
// Context
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// Layanan eksternal
|
||||
dbService database.Service
|
||||
|
||||
// Monitoring
|
||||
startTime time.Time
|
||||
messageCount int64
|
||||
errorCount int64
|
||||
activityLog []ActivityLog
|
||||
activityMu sync.RWMutex
|
||||
|
||||
// Registry handler pesan
|
||||
messageRegistry *MessageRegistry
|
||||
broadcaster *Broadcaster
|
||||
}
|
||||
|
||||
// ActivityLog menyimpan log aktivitas
|
||||
type ActivityLog struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Event string `json:"event"`
|
||||
ClientID string `json:"client_id"`
|
||||
Details string `json:"details"`
|
||||
}
|
||||
|
||||
// DatabaseService mendefinisikan interface untuk layanan database
|
||||
type DatabaseService interface {
|
||||
Health() map[string]interface{}
|
||||
ListDBs() []string
|
||||
ListenForChanges(ctx context.Context, dbName string, channels []string, callback func(channel, payload string)) error
|
||||
NotifyChange(dbName, channel, payload string) error
|
||||
GetDB(name string) (*sql.DB, error)
|
||||
GetPrimaryDB(name string) (*sql.DB, error)
|
||||
}
|
||||
|
||||
// Global database service instance
|
||||
var (
|
||||
dbService database.Service
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// Initialize the database connection
|
||||
func init() {
|
||||
once.Do(func() {
|
||||
dbService = database.New(config.LoadConfig())
|
||||
if dbService == nil {
|
||||
panic("Failed to initialize database connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// NewHub membuat hub baru dengan konfigurasi yang diberikan
|
||||
func NewHub(config config.WebSocketConfig) *Hub {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
hub := &Hub{
|
||||
|
||||
config: config,
|
||||
clients: make(map[*Client]bool),
|
||||
clientsByID: make(map[string]*Client),
|
||||
clientsByIP: make(map[string][]*Client),
|
||||
clientsByStatic: make(map[string]*Client),
|
||||
rooms: make(map[string]map[*Client]bool),
|
||||
broadcast: make(chan WebSocketMessage, 1000),
|
||||
register: make(chan *Client),
|
||||
unregister: make(chan *Client),
|
||||
messageQueue: make(chan WebSocketMessage, config.MessageQueueSize),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
dbService: dbService,
|
||||
startTime: time.Now(),
|
||||
activityLog: make([]ActivityLog, 0, config.ActivityLogSize),
|
||||
messageRegistry: NewMessageRegistry(),
|
||||
}
|
||||
|
||||
// Setup default message handlers
|
||||
// SetupDefaultHandlers(hub.messageRegistry)
|
||||
|
||||
// Setup database change listeners
|
||||
hub.setupDatabaseListeners()
|
||||
|
||||
return hub
|
||||
}
|
||||
|
||||
// Run menjalankan loop utama hub
|
||||
func (h *Hub) Run() {
|
||||
// Start queue workers
|
||||
for i := 0; i < h.config.QueueWorkers; i++ {
|
||||
go h.queueWorker(i)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case client := <-h.register:
|
||||
h.registerClient(client)
|
||||
|
||||
case client := <-h.unregister:
|
||||
h.unregisterClient(client)
|
||||
|
||||
case message := <-h.broadcast:
|
||||
h.messageCount++
|
||||
h.broadcastToClients(message)
|
||||
|
||||
case <-h.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// queueWorker memproses pesan dari antrian
|
||||
func (h *Hub) queueWorker(workerID int) {
|
||||
for {
|
||||
select {
|
||||
case message := <-h.messageQueue:
|
||||
h.broadcast <- message
|
||||
case <-h.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerClient mendaftarkan klien baru
|
||||
func (h *Hub) registerClient(client *Client) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// Register di peta klien utama
|
||||
h.clients[client] = true
|
||||
|
||||
// Register berdasarkan ID
|
||||
h.clientsByID[client.ID] = client
|
||||
|
||||
// Register berdasarkan ID statis
|
||||
if client.StaticID != "" {
|
||||
h.clientsByStatic[client.StaticID] = client
|
||||
}
|
||||
|
||||
// Register berdasarkan IP
|
||||
if h.clientsByIP[client.IPAddress] == nil {
|
||||
h.clientsByIP[client.IPAddress] = make([]*Client, 0)
|
||||
}
|
||||
h.clientsByIP[client.IPAddress] = append(h.clientsByIP[client.IPAddress], client)
|
||||
|
||||
// Register di ruangan
|
||||
if client.Room != "" {
|
||||
if h.rooms[client.Room] == nil {
|
||||
h.rooms[client.Room] = make(map[*Client]bool)
|
||||
}
|
||||
h.rooms[client.Room][client] = true
|
||||
}
|
||||
|
||||
// Log aktivitas
|
||||
h.logActivity("client_connected", client.ID,
|
||||
fmt.Sprintf("IP: %s, Static: %s, Room: %s", client.IPAddress, client.StaticID, client.Room))
|
||||
}
|
||||
|
||||
// unregisterClient menghapus klien
|
||||
func (h *Hub) unregisterClient(client *Client) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if _, ok := h.clients[client]; ok {
|
||||
// Hapus dari peta klien utama
|
||||
delete(h.clients, client)
|
||||
close(client.Send)
|
||||
|
||||
// Hapus dari peta berdasarkan ID
|
||||
delete(h.clientsByID, client.ID)
|
||||
|
||||
// Hapus dari peta berdasarkan ID statis
|
||||
if client.StaticID != "" {
|
||||
delete(h.clientsByStatic, client.StaticID)
|
||||
}
|
||||
|
||||
// Hapus dari peta berdasarkan IP
|
||||
if ipClients, exists := h.clientsByIP[client.IPAddress]; exists {
|
||||
for i, c := range ipClients {
|
||||
if c == client {
|
||||
h.clientsByIP[client.IPAddress] = append(ipClients[:i], ipClients[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Jika tidak ada lagi klien dari IP ini, hapus entri IP
|
||||
if len(h.clientsByIP[client.IPAddress]) == 0 {
|
||||
delete(h.clientsByIP, client.IPAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus dari ruangan
|
||||
if client.Room != "" {
|
||||
if room, exists := h.rooms[client.Room]; exists {
|
||||
delete(room, client)
|
||||
if len(room) == 0 {
|
||||
delete(h.rooms, client.Room)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log aktivitas
|
||||
h.logActivity("client_disconnected", client.ID,
|
||||
fmt.Sprintf("IP: %s, Duration: %v", client.IPAddress, time.Since(client.connectedAt)))
|
||||
|
||||
client.cancel()
|
||||
}
|
||||
|
||||
// broadcastToClients mengirim pesan ke klien yang sesuai
|
||||
func (h *Hub) broadcastToClients(message WebSocketMessage) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
if message.ClientID != "" {
|
||||
// Kirim ke klien tertentu
|
||||
if client, exists := h.clientsByID[message.ClientID]; exists {
|
||||
select {
|
||||
case client.Send <- message:
|
||||
default:
|
||||
go func() {
|
||||
h.unregister <- client
|
||||
}()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Periksa apakah ini pesan ruangan
|
||||
if message.Room != "" {
|
||||
if room, roomExists := h.rooms[message.Room]; roomExists {
|
||||
for client := range room {
|
||||
select {
|
||||
case client.Send <- message:
|
||||
default:
|
||||
go func(c *Client) {
|
||||
h.unregister <- c
|
||||
}(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Broadcast ke semua klien
|
||||
for client := range h.clients {
|
||||
select {
|
||||
case client.Send <- message:
|
||||
default:
|
||||
go func(c *Client) {
|
||||
h.unregister <- c
|
||||
}(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logActivity mencatat aktivitas
|
||||
func (h *Hub) logActivity(event, clientID, details string) {
|
||||
h.activityMu.Lock()
|
||||
defer h.activityMu.Unlock()
|
||||
|
||||
activity := ActivityLog{
|
||||
Timestamp: time.Now(),
|
||||
Event: event,
|
||||
ClientID: clientID,
|
||||
Details: details,
|
||||
}
|
||||
|
||||
h.activityLog = append(h.activityLog, activity)
|
||||
|
||||
// Pertahankan hanya aktivitas terakhir sesuai konfigurasi
|
||||
if len(h.activityLog) > h.config.ActivityLogSize {
|
||||
h.activityLog = h.activityLog[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig mengembalikan konfigurasi hub
|
||||
func (h *Hub) GetConfig() config.WebSocketConfig {
|
||||
return h.config
|
||||
}
|
||||
|
||||
// GetMessageRegistry mengembalikan registry pesan
|
||||
func (h *Hub) GetMessageRegistry() *MessageRegistry {
|
||||
return h.messageRegistry
|
||||
}
|
||||
|
||||
// RegisterChannel mengembalikan channel register untuk handler
|
||||
func (h *Hub) RegisterChannel() chan *Client {
|
||||
return h.register
|
||||
}
|
||||
|
||||
// GetStats mengembalikan statistik hub
|
||||
func (h *Hub) GetStats() map[string]interface{} {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"connected_clients": len(h.clients),
|
||||
"unique_ips": len(h.clientsByIP),
|
||||
"static_clients": len(h.clientsByStatic),
|
||||
"rooms": len(h.rooms),
|
||||
"message_count": h.messageCount,
|
||||
"error_count": h.errorCount,
|
||||
"uptime": time.Since(h.startTime).String(),
|
||||
"server_id": h.config.ServerID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// setupDatabaseListeners sets up database change listeners for real-time updates
|
||||
func (h *Hub) setupDatabaseListeners() {
|
||||
// Listen for changes on retribusi table
|
||||
channels := []string{"retribusi_changes", "data_changes"}
|
||||
|
||||
err := h.dbService.ListenForChanges(h.ctx, "postgres_satudata", channels, func(channel, payload string) {
|
||||
h.handleDatabaseChange(channel, payload)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
h.logActivity("database_listener_error", "", fmt.Sprintf("Failed to setup listeners: %v", err))
|
||||
} else {
|
||||
h.logActivity("database_listener_started", "", "Database change listeners initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// handleDatabaseChange processes database change notifications
|
||||
func (h *Hub) handleDatabaseChange(channel, payload string) {
|
||||
h.logActivity("database_change", "", fmt.Sprintf("Channel: %s, Payload: %s", channel, payload))
|
||||
|
||||
// Parse the payload to determine what changed
|
||||
var changeData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(payload), &changeData); err != nil {
|
||||
h.logActivity("database_change_parse_error", "", fmt.Sprintf("Failed to parse payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create WebSocket message for broadcasting
|
||||
message := WebSocketMessage{
|
||||
Type: DatabaseChangeMessage,
|
||||
Data: changeData,
|
||||
ClientID: "",
|
||||
Room: "data_updates", // Broadcast to data_updates room
|
||||
}
|
||||
|
||||
// Broadcast the change to all connected clients in the data_updates room
|
||||
h.broadcast <- message
|
||||
}
|
||||
|
||||
// GetDatabaseConnection returns a database connection for the specified database
|
||||
func (h *Hub) GetDatabaseConnection(dbName string) (*sql.DB, error) {
|
||||
return h.dbService.GetDB(dbName)
|
||||
}
|
||||
|
||||
// ExecuteDatabaseQuery executes a database query and returns results
|
||||
func (h *Hub) ExecuteDatabaseQuery(dbName, query string, args ...interface{}) ([]map[string]interface{}, error) {
|
||||
db, err := h.GetDatabaseConnection(dbName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get database connection: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(h.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get columns: %w", err)
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
|
||||
for rows.Next() {
|
||||
values := make([]interface{}, len(columns))
|
||||
valuePtrs := make([]interface{}, len(columns))
|
||||
for i := range values {
|
||||
valuePtrs[i] = &values[i]
|
||||
}
|
||||
|
||||
if err := rows.Scan(valuePtrs...); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan row: %w", err)
|
||||
}
|
||||
|
||||
row := make(map[string]interface{})
|
||||
for i, col := range columns {
|
||||
val := values[i]
|
||||
if b, ok := val.([]byte); ok {
|
||||
val = string(b)
|
||||
}
|
||||
row[col] = val
|
||||
}
|
||||
results = append(results, row)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// NotifyDatabaseChange sends a notification to the database for real-time updates
|
||||
func (h *Hub) NotifyDatabaseChange(dbName, channel, payload string) error {
|
||||
return h.dbService.NotifyChange(dbName, channel, payload)
|
||||
}
|
||||
109
internal/services/websocket/message.go
Normal file
109
internal/services/websocket/message.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MessageType mendefinisikan tipe-tipe pesan yang valid
|
||||
type MessageType string
|
||||
|
||||
const (
|
||||
// Pesan koneksi
|
||||
WelcomeMessage MessageType = "welcome"
|
||||
ErrorMessage MessageType = "error"
|
||||
DisconnectMessage MessageType = "disconnect"
|
||||
|
||||
// Pesan kontrol
|
||||
PingMessage MessageType = "ping"
|
||||
PongMessage MessageType = "pong"
|
||||
HeartbeatMessage MessageType = "heartbeat"
|
||||
ConnectionTestMessage MessageType = "connection_test"
|
||||
|
||||
// Pesan informasi
|
||||
ServerInfoMessage MessageType = "server_info"
|
||||
ClientInfoMessage MessageType = "client_info"
|
||||
OnlineUsersMessage MessageType = "online_users"
|
||||
|
||||
// Pesan komunikasi
|
||||
DirectMessage MessageType = "direct_message"
|
||||
RoomMessage MessageType = "room_message"
|
||||
BroadcastMessage MessageType = "broadcast"
|
||||
|
||||
// Pesan database
|
||||
DatabaseQueryMessage MessageType = "database_query"
|
||||
DatabaseChangeMessage MessageType = "database_change"
|
||||
DatabaseInsertMessage MessageType = "db_insert"
|
||||
DatabaseCustomQueryMessage MessageType = "db_custom_query"
|
||||
|
||||
// Pesan monitoring
|
||||
SystemStatusMessage MessageType = "system_status"
|
||||
DataStreamMessage MessageType = "data_stream"
|
||||
|
||||
// Pesan room management
|
||||
JoinRoomMessage MessageType = "join_room"
|
||||
LeaveRoomMessage MessageType = "leave_room"
|
||||
GetRoomInfoMessage MessageType = "get_room_info"
|
||||
|
||||
// Pesan admin
|
||||
AdminKickClientMessage MessageType = "admin_kick_client"
|
||||
AdminKillServerMessage MessageType = "admin_kill_server"
|
||||
GetServerStatsMessage MessageType = "get_server_stats"
|
||||
GetSystemHealthMessage MessageType = "get_system_health"
|
||||
AdminClearLogsMessage MessageType = "admin_clear_logs"
|
||||
|
||||
// Pesan utility
|
||||
GetStatsMessage MessageType = "get_stats"
|
||||
)
|
||||
|
||||
// WebSocketMessage menyimpan struktur pesan WebSocket
|
||||
type WebSocketMessage struct {
|
||||
Type MessageType `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
Room string `json:"room,omitempty"`
|
||||
}
|
||||
|
||||
// NewWebSocketMessage membuat pesan WebSocket baru
|
||||
func NewWebSocketMessage(msgType MessageType, data interface{}, clientID, room string) WebSocketMessage {
|
||||
return WebSocketMessage{
|
||||
Type: msgType,
|
||||
Data: data,
|
||||
Timestamp: time.Now(),
|
||||
ClientID: clientID,
|
||||
MessageID: uuid.New().String(),
|
||||
Room: room,
|
||||
}
|
||||
}
|
||||
|
||||
// MessageHandler mendefinisikan interface untuk handler pesan
|
||||
type MessageHandler interface {
|
||||
HandleMessage(client *Client, message WebSocketMessage) error
|
||||
MessageType() MessageType
|
||||
}
|
||||
|
||||
// MessageRegistry menyimpan registry untuk handler pesan
|
||||
type MessageRegistry struct {
|
||||
handlers map[MessageType]MessageHandler
|
||||
}
|
||||
|
||||
// NewMessageRegistry membuat registry pesan baru
|
||||
func NewMessageRegistry() *MessageRegistry {
|
||||
return &MessageRegistry{
|
||||
handlers: make(map[MessageType]MessageHandler),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandler mendaftarkan handler untuk tipe pesan tertentu
|
||||
func (r *MessageRegistry) RegisterHandler(handler MessageHandler) {
|
||||
r.handlers[handler.MessageType()] = handler
|
||||
}
|
||||
|
||||
// GetHandler mendapatkan handler untuk tipe pesan tertentu
|
||||
func (r *MessageRegistry) GetHandler(msgType MessageType) (MessageHandler, bool) {
|
||||
handler, exists := r.handlers[msgType]
|
||||
return handler, exists
|
||||
}
|
||||
211
internal/services/websocket/monitor.go
Normal file
211
internal/services/websocket/monitor.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DetailedStats menyimpan statistik detail WebSocket
|
||||
type DetailedStats struct {
|
||||
ConnectedClients int `json:"connected_clients"`
|
||||
UniqueIPs int `json:"unique_ips"`
|
||||
StaticClients int `json:"static_clients"`
|
||||
ActiveRooms int `json:"active_rooms"`
|
||||
IPDistribution map[string]int `json:"ip_distribution"`
|
||||
RoomDistribution map[string]int `json:"room_distribution"`
|
||||
MessageQueueSize int `json:"message_queue_size"`
|
||||
QueueWorkers int `json:"queue_workers"`
|
||||
Uptime time.Duration `json:"uptime_seconds"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ClientInfo menyimpan informasi klien
|
||||
type ClientInfo struct {
|
||||
ID string `json:"id"`
|
||||
StaticID string `json:"static_id"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
UserID string `json:"user_id"`
|
||||
Room string `json:"room"`
|
||||
ConnectedAt time.Time `json:"connected_at"`
|
||||
LastPing time.Time `json:"last_ping"`
|
||||
LastPong time.Time `json:"last_pong"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// MonitoringData menyimpan data monitoring lengkap
|
||||
type MonitoringData struct {
|
||||
Stats DetailedStats `json:"stats"`
|
||||
RecentActivity []ActivityLog `json:"recent_activity"`
|
||||
SystemHealth map[string]interface{} `json:"system_health"`
|
||||
Performance PerformanceMetrics `json:"performance"`
|
||||
}
|
||||
|
||||
// PerformanceMetrics menyimpan metrik performa
|
||||
type PerformanceMetrics struct {
|
||||
MessagesPerSecond float64 `json:"messages_per_second"`
|
||||
AverageLatency float64 `json:"average_latency_ms"`
|
||||
ErrorRate float64 `json:"error_rate_percent"`
|
||||
MemoryUsage int64 `json:"memory_usage_bytes"`
|
||||
}
|
||||
|
||||
// MonitoringManager mengelola monitoring WebSocket
|
||||
type MonitoringManager struct {
|
||||
hub *Hub
|
||||
}
|
||||
|
||||
// NewMonitoringManager membuat manajer monitoring baru
|
||||
func NewMonitoringManager(hub *Hub) *MonitoringManager {
|
||||
return &MonitoringManager{hub: hub}
|
||||
}
|
||||
|
||||
// GetDetailedStats mengembalikan statistik detail
|
||||
func (m *MonitoringManager) GetDetailedStats() DetailedStats {
|
||||
m.hub.mu.RLock()
|
||||
defer m.hub.mu.RUnlock()
|
||||
|
||||
// Hitung distribusi IP
|
||||
ipDistribution := make(map[string]int)
|
||||
for ip, clients := range m.hub.clientsByIP {
|
||||
ipDistribution[ip] = len(clients)
|
||||
}
|
||||
|
||||
// Hitung distribusi ruangan
|
||||
roomDistribution := make(map[string]int)
|
||||
for room, clients := range m.hub.rooms {
|
||||
roomDistribution[room] = len(clients)
|
||||
}
|
||||
|
||||
return DetailedStats{
|
||||
ConnectedClients: len(m.hub.clients),
|
||||
UniqueIPs: len(m.hub.clientsByIP),
|
||||
StaticClients: len(m.hub.clientsByStatic),
|
||||
ActiveRooms: len(m.hub.rooms),
|
||||
IPDistribution: ipDistribution,
|
||||
RoomDistribution: roomDistribution,
|
||||
MessageQueueSize: len(m.hub.messageQueue),
|
||||
QueueWorkers: m.hub.config.QueueWorkers,
|
||||
Uptime: time.Since(m.hub.startTime),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllClients mengembalikan semua klien yang terhubung
|
||||
func (m *MonitoringManager) GetAllClients() []ClientInfo {
|
||||
m.hub.mu.RLock()
|
||||
defer m.hub.mu.RUnlock()
|
||||
|
||||
var clients []ClientInfo
|
||||
for client := range m.hub.clients {
|
||||
clientInfo := ClientInfo{
|
||||
ID: client.ID,
|
||||
StaticID: client.StaticID,
|
||||
IPAddress: client.IPAddress,
|
||||
UserID: client.UserID,
|
||||
Room: client.Room,
|
||||
ConnectedAt: client.connectedAt,
|
||||
LastPing: client.lastPing,
|
||||
LastPong: client.lastPong,
|
||||
IsActive: client.isClientActive(),
|
||||
}
|
||||
clients = append(clients, clientInfo)
|
||||
}
|
||||
|
||||
return clients
|
||||
}
|
||||
|
||||
// GetMonitoringData mengembalikan data monitoring lengkap
|
||||
func (m *MonitoringManager) GetMonitoringData() MonitoringData {
|
||||
stats := m.GetDetailedStats()
|
||||
|
||||
m.hub.activityMu.RLock()
|
||||
recentActivity := make([]ActivityLog, 0)
|
||||
// Dapatkan 100 aktivitas terakhir
|
||||
start := len(m.hub.activityLog) - 100
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for i := start; i < len(m.hub.activityLog); i++ {
|
||||
recentActivity = append(recentActivity, m.hub.activityLog[i])
|
||||
}
|
||||
m.hub.activityMu.RUnlock()
|
||||
|
||||
// Dapatkan kesehatan sistem dari layanan database
|
||||
systemHealth := make(map[string]interface{})
|
||||
if m.hub.dbService != nil {
|
||||
systemHealth["databases"] = m.hub.dbService.Health()
|
||||
systemHealth["available_dbs"] = m.hub.dbService.ListDBs()
|
||||
}
|
||||
systemHealth["websocket_status"] = "healthy"
|
||||
systemHealth["uptime_seconds"] = time.Since(m.hub.startTime).Seconds()
|
||||
|
||||
// Hitung metrik performa
|
||||
uptime := time.Since(m.hub.startTime)
|
||||
var messagesPerSecond float64
|
||||
var errorRate float64
|
||||
|
||||
if uptime.Seconds() > 0 {
|
||||
messagesPerSecond = float64(m.hub.messageCount) / uptime.Seconds()
|
||||
}
|
||||
|
||||
if m.hub.messageCount > 0 {
|
||||
errorRate = (float64(m.hub.errorCount) / float64(m.hub.messageCount)) * 100
|
||||
}
|
||||
|
||||
performance := PerformanceMetrics{
|
||||
MessagesPerSecond: messagesPerSecond,
|
||||
AverageLatency: 2.5, // Nilai mock - implementasi pelacakan latensi aktual
|
||||
ErrorRate: errorRate,
|
||||
MemoryUsage: 0, // Nilai mock - implementasi pelacakan memori aktual
|
||||
}
|
||||
|
||||
return MonitoringData{
|
||||
Stats: stats,
|
||||
RecentActivity: recentActivity,
|
||||
SystemHealth: systemHealth,
|
||||
Performance: performance,
|
||||
}
|
||||
}
|
||||
|
||||
// CleanupInactiveClients membersihkan klien tidak aktif
|
||||
func (m *MonitoringManager) CleanupInactiveClients(inactiveTimeout time.Duration) int {
|
||||
m.hub.mu.RLock()
|
||||
var inactiveClients []*Client
|
||||
cutoff := time.Now().Add(-inactiveTimeout)
|
||||
|
||||
for client := range m.hub.clients {
|
||||
if client.lastPing.Before(cutoff) {
|
||||
inactiveClients = append(inactiveClients, client)
|
||||
}
|
||||
}
|
||||
m.hub.mu.RUnlock()
|
||||
|
||||
// Putuskan koneksi klien tidak aktif
|
||||
for _, client := range inactiveClients {
|
||||
m.hub.logActivity("cleanup_disconnect", client.ID,
|
||||
fmt.Sprintf("Inactive for %v", time.Since(client.lastPing)))
|
||||
client.cancel()
|
||||
client.Conn.Close()
|
||||
}
|
||||
|
||||
return len(inactiveClients)
|
||||
}
|
||||
|
||||
// DisconnectClient memutuskan koneksi klien tertentu
|
||||
func (m *MonitoringManager) DisconnectClient(clientID string) bool {
|
||||
m.hub.mu.RLock()
|
||||
client, exists := m.hub.clientsByID[clientID]
|
||||
m.hub.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
// Log aktivitas
|
||||
m.hub.logActivity("force_disconnect", clientID, "Client disconnected by admin")
|
||||
|
||||
// Batalkan context dan tutup koneksi
|
||||
client.cancel()
|
||||
client.Conn.Close()
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user