@@ -139,7 +139,10 @@ type SubscriptionProtocol interface {
139
139
// SubscriptionContext represents a shared context for protocol implementations with the websocket connection inside.
140
140
type SubscriptionContext struct {
141
141
context.Context
142
- client * SubscriptionClient
142
+
143
+ client * SubscriptionClient
144
+ // The unique id across the session life-cycle.
145
+ sessionID uuid.UUID
143
146
websocketConn WebsocketConn
144
147
145
148
connectionInitAt time.Time
@@ -154,10 +157,15 @@ type SubscriptionContext struct {
154
157
// Log prints condition logging with message type filters.
155
158
func (sc * SubscriptionContext ) Log (
156
159
message interface {},
157
- source string ,
160
+ metadata map [ string ] any ,
158
161
opType OperationMessageType ,
159
162
) {
160
- sc .client .printLog (message , source , opType )
163
+ metadata ["session_id" ] = sc .sessionID
164
+ if conn , ok := sc .websocketConn .(* WebsocketHandler ); ok {
165
+ metadata ["connection_id" ] = conn .GetID ()
166
+ }
167
+
168
+ sc .client .printLog (message , metadata , opType )
161
169
}
162
170
163
171
// OnConnectionAlive executes the OnConnectionAlive callback if exists.
@@ -348,8 +356,12 @@ func (sc *SubscriptionContext) Close() error {
348
356
349
357
var err error
350
358
sc .SetClosed (true )
359
+ conn := sc .GetWebsocketConn ()
360
+ sc .Log ("closing the current session" , map [string ]any {
361
+ "connection_active" : conn != nil ,
362
+ }, GQLInternal )
351
363
352
- if conn := sc . GetWebsocketConn (); conn != nil {
364
+ if conn != nil {
353
365
if sc .client .onDisconnected != nil {
354
366
sc .client .onDisconnected ()
355
367
}
@@ -369,7 +381,9 @@ func (sc *SubscriptionContext) Close() error {
369
381
// Send emits a message to the graphql server.
370
382
func (sc * SubscriptionContext ) Send (message interface {}, opType OperationMessageType ) error {
371
383
if conn := sc .GetWebsocketConn (); conn != nil {
372
- sc .Log (message , "client" , opType )
384
+ sc .Log (message , map [string ]any {
385
+ "source" : "client" ,
386
+ }, opType )
373
387
374
388
return conn .WriteJSON (message )
375
389
}
@@ -379,6 +393,7 @@ func (sc *SubscriptionContext) Send(message interface{}, opType OperationMessage
379
393
380
394
// initializes the websocket connection.
381
395
func (sc * SubscriptionContext ) init (parentContext context.Context ) error {
396
+ sc .Log ("initialize new session" , map [string ]any {}, GQLInternal )
382
397
now := time .Now ()
383
398
384
399
for {
@@ -423,7 +438,9 @@ func (sc *SubscriptionContext) init(parentContext context.Context) error {
423
438
424
439
sc .Log (
425
440
fmt .Sprintf ("%s. retry in %d second..." , err .Error (), sc .client .retryDelay / time .Second ),
426
- "client" ,
441
+ map [string ]any {
442
+ "source" : "client" ,
443
+ },
427
444
GQLInternal ,
428
445
)
429
446
time .Sleep (sc .client .retryDelay )
@@ -448,7 +465,9 @@ func (sc *SubscriptionContext) run() {
448
465
if errors .Is (err , io .EOF ) || strings .Contains (err .Error (), "EOF" ) ||
449
466
errors .Is (err , net .ErrClosed ) ||
450
467
strings .Contains (err .Error (), "connection reset by peer" ) {
451
- sc .Log (err .Error (), "client" , GQLConnectionError )
468
+ sc .Log (err .Error (), map [string ]any {
469
+ "source" : "client" ,
470
+ }, GQLConnectionError )
452
471
sc .client .errorChan <- errRestartSubscriptionClient
453
472
454
473
return
@@ -470,7 +489,9 @@ func (sc *SubscriptionContext) run() {
470
489
}
471
490
472
491
if closeStatus < 0 {
473
- sc .Log (err , "server" , GQL_CONNECTION_ERROR )
492
+ sc .Log (err , map [string ]any {
493
+ "source" : "client" ,
494
+ }, GQL_CONNECTION_ERROR )
474
495
475
496
continue
476
497
}
@@ -482,14 +503,18 @@ func (sc *SubscriptionContext) run() {
482
503
websocket .StatusTryAgainLater ,
483
504
websocket .StatusMessageTooBig ,
484
505
websocket .StatusInvalidFramePayloadData :
485
- sc .Log (err , "server" , GQL_CONNECTION_ERROR )
506
+ sc .Log (err , map [string ]any {
507
+ "source" : "server" ,
508
+ }, GQL_CONNECTION_ERROR )
486
509
sc .client .errorChan <- errRestartSubscriptionClient
487
510
case websocket .StatusNormalClosure , websocket .StatusAbnormalClosure :
488
511
// close event from websocket client, exiting...
489
512
_ = sc .client .close (sc )
490
513
default :
491
514
// let the user to handle unknown errors manually.
492
- sc .Log (err , "server" , GQL_CONNECTION_ERROR )
515
+ sc .Log (err , map [string ]any {
516
+ "source" : "server" ,
517
+ }, GQL_CONNECTION_ERROR )
493
518
sc .client .errorChan <- err
494
519
}
495
520
@@ -531,7 +556,9 @@ func (sc *SubscriptionContext) startWebsocketKeepAlive(c WebsocketConn, interval
531
556
// Ping the websocket. You might want to handle any potential errors.
532
557
err := c .Ping ()
533
558
if err != nil {
534
- sc .Log ("Failed to ping server" , "client" , GQLInternal )
559
+ sc .Log ("Failed to ping server" , map [string ]any {
560
+ "source" : "client" ,
561
+ }, GQLInternal )
535
562
sc .client .errorChan <- errRestartSubscriptionClient
536
563
537
564
return
@@ -1137,7 +1164,9 @@ func (sc *SubscriptionClient) RunWithContext(ctx context.Context) error {
1137
1164
time .Since (
1138
1165
session .getConnectionInitAt (),
1139
1166
) > sc .connectionInitialisationTimeout {
1140
- sc .printLog ("Connection initialisation timeout" , "client" , GQLInternal )
1167
+ sc .printLog ("Connection initialisation timeout" , map [string ]any {
1168
+ "source" : "client" ,
1169
+ }, GQLInternal )
1141
1170
sc .errorChan <- & websocket.CloseError {
1142
1171
Code : StatusConnectionInitialisationTimeout ,
1143
1172
Reason : "Connection initialisation timeout" ,
@@ -1152,7 +1181,9 @@ func (sc *SubscriptionClient) RunWithContext(ctx context.Context) error {
1152
1181
) > sc .websocketConnectionIdleTimeout {
1153
1182
sc .printLog (
1154
1183
ErrWebsocketConnectionIdleTimeout .Error (),
1155
- "client" ,
1184
+ map [string ]any {
1185
+ "source" : "client" ,
1186
+ },
1156
1187
GQLInternal ,
1157
1188
)
1158
1189
sc .errorChan <- ErrWebsocketConnectionIdleTimeout
@@ -1269,6 +1300,8 @@ func (sc *SubscriptionClient) close(session *SubscriptionContext) error {
1269
1300
return nil
1270
1301
}
1271
1302
1303
+ sc .printLog ("close the subscription client" , map [string ]any {}, GQLInternal )
1304
+
1272
1305
sc .setClientStatus (scStatusClosing )
1273
1306
if sc .cancel != nil {
1274
1307
sc .cancel ()
@@ -1323,6 +1356,7 @@ func (sc *SubscriptionClient) initNewSession(ctx context.Context) (*Subscription
1323
1356
1324
1357
subContext := & SubscriptionContext {
1325
1358
client : sc ,
1359
+ sessionID : uuid .New (),
1326
1360
subscriptions : make (map [string ]Subscription ),
1327
1361
}
1328
1362
@@ -1380,15 +1414,17 @@ func (sc *SubscriptionClient) checkSubscriptionStatuses(session *SubscriptionCon
1380
1414
SubscriptionRunning ,
1381
1415
SubscriptionWaiting ,
1382
1416
}) == 0 {
1383
- session .Log ("no running subscription. exiting..." , "client" , GQLInternal )
1417
+ session .Log ("no running subscription. exiting..." , map [string ]any {
1418
+ "source" : "client" ,
1419
+ }, GQLInternal )
1384
1420
_ = sc .close (session )
1385
1421
}
1386
1422
}
1387
1423
1388
1424
// prints condition logging with message type filters.
1389
1425
func (sc * SubscriptionClient ) printLog (
1390
1426
message interface {},
1391
- source string ,
1427
+ metadata map [ string ] any ,
1392
1428
opType OperationMessageType ,
1393
1429
) {
1394
1430
if sc .log == nil {
@@ -1401,7 +1437,8 @@ func (sc *SubscriptionClient) printLog(
1401
1437
}
1402
1438
}
1403
1439
1404
- sc .log (message , source )
1440
+ metadata ["type" ] = opType
1441
+ sc .log (message , metadata )
1405
1442
}
1406
1443
1407
1444
// The payload format of both subscriptions-transport-ws and graphql-ws are the same.
@@ -1453,11 +1490,20 @@ func parseInt32Ranges(codes []string) ([][]int32, error) {
1453
1490
type WebsocketHandler struct {
1454
1491
* websocket.Conn
1455
1492
1493
+ // The unique id of the current websocket connections.
1494
+ // The ID will be generated whenever initializing a new Websocket connection.
1495
+ id uuid.UUID
1496
+
1456
1497
ctx context.Context
1457
1498
readTimeout time.Duration
1458
1499
writeTimeout time.Duration
1459
1500
}
1460
1501
1502
+ // GetID gets the identity of the Websocket connection.
1503
+ func (wh WebsocketHandler ) GetID () uuid.UUID {
1504
+ return wh .id
1505
+ }
1506
+
1461
1507
// WriteJSON implements the function to encode and send message in json format to the server.
1462
1508
func (wh * WebsocketHandler ) WriteJSON (v interface {}) error {
1463
1509
ctx , cancel := context .WithTimeout (wh .ctx , wh .writeTimeout )
@@ -1484,6 +1530,8 @@ func (wh *WebsocketHandler) Ping() error {
1484
1530
1485
1531
// Close implements the function to close the websocket connection.
1486
1532
func (wh * WebsocketHandler ) Close () error {
1533
+ globalWebSocketStats .AddDeadConnection (wh .id )
1534
+
1487
1535
return wh .Conn .Close (websocket .StatusNormalClosure , "close websocket" )
1488
1536
}
1489
1537
@@ -1528,8 +1576,12 @@ func newWebsocketConn(
1528
1576
return nil , err
1529
1577
}
1530
1578
1579
+ id := uuid .New ()
1580
+ globalWebSocketStats .AddActiveConnection (id )
1581
+
1531
1582
return & WebsocketHandler {
1532
1583
Conn : c ,
1584
+ id : id ,
1533
1585
ctx : ctx ,
1534
1586
readTimeout : options .ReadTimeout ,
1535
1587
writeTimeout : options .WriteTimeout ,
@@ -1572,3 +1624,60 @@ type WebsocketOptions struct {
1572
1624
// The graphql server depends on the Sec-WebSocket-Protocol header to return the correct message specification
1573
1625
Subprotocols []string
1574
1626
}
1627
+
1628
+ // WebSocketStats hold statistic data of WebSocket connections for subscription.
1629
+ type WebSocketStats struct {
1630
+ TotalActiveConnections int
1631
+ TotalClosedConnections int
1632
+ ActiveConnectionIDs []uuid.UUID
1633
+ }
1634
+
1635
+ type websocketStats struct {
1636
+ sync sync.Mutex
1637
+ activeConnectionIDs map [uuid.UUID ]bool
1638
+ closedConnectionIDs map [uuid.UUID ]bool
1639
+ }
1640
+
1641
+ var globalWebSocketStats = websocketStats {
1642
+ activeConnectionIDs : map [uuid.UUID ]bool {},
1643
+ closedConnectionIDs : map [uuid.UUID ]bool {},
1644
+ }
1645
+
1646
+ // AddActiveConnection adds an active connection id to the list.
1647
+ func (ws * websocketStats ) AddActiveConnection (id uuid.UUID ) {
1648
+ ws .sync .Lock ()
1649
+ defer ws .sync .Unlock ()
1650
+
1651
+ ws .activeConnectionIDs [id ] = true
1652
+ }
1653
+
1654
+ // AddDeadConnection adds an dead connection id to the list.
1655
+ func (ws * websocketStats ) AddDeadConnection (id uuid.UUID ) {
1656
+ ws .sync .Lock ()
1657
+ defer ws .sync .Unlock ()
1658
+ delete (ws .activeConnectionIDs , id )
1659
+
1660
+ ws .closedConnectionIDs [id ] = true
1661
+ }
1662
+
1663
+ // GetStats gets the websocket stats.
1664
+ func (ws * websocketStats ) GetStats () WebSocketStats {
1665
+ ws .sync .Lock ()
1666
+ defer ws .sync .Unlock ()
1667
+
1668
+ activeIDs := make ([]uuid.UUID , 0 , len (ws .activeConnectionIDs ))
1669
+ for id := range ws .activeConnectionIDs {
1670
+ activeIDs = append (activeIDs , id )
1671
+ }
1672
+
1673
+ return WebSocketStats {
1674
+ ActiveConnectionIDs : activeIDs ,
1675
+ TotalActiveConnections : len (globalWebSocketStats .activeConnectionIDs ),
1676
+ TotalClosedConnections : len (globalWebSocketStats .closedConnectionIDs ),
1677
+ }
1678
+ }
1679
+
1680
+ // GetWebSocketStats gets the websocket stats.
1681
+ func GetWebSocketStats () WebSocketStats {
1682
+ return globalWebSocketStats .GetStats ()
1683
+ }
0 commit comments