9
9
"net"
10
10
"net/http"
11
11
"net/netip"
12
+ "reflect"
12
13
"strconv"
13
14
"strings"
14
15
"time"
@@ -18,6 +19,7 @@ import (
18
19
"golang.org/x/mod/semver"
19
20
"golang.org/x/xerrors"
20
21
"nhooyr.io/websocket"
22
+ "nhooyr.io/websocket/wsjson"
21
23
"tailscale.com/tailcfg"
22
24
23
25
"cdr.dev/slog"
@@ -739,6 +741,127 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator *tailnet.Coordi
739
741
740
742
return workspaceAgent , nil
741
743
}
744
+ func (api * API ) workspaceAgentReportStats (rw http.ResponseWriter , r * http.Request ) {
745
+ api .websocketWaitMutex .Lock ()
746
+ api .websocketWaitGroup .Add (1 )
747
+ api .websocketWaitMutex .Unlock ()
748
+ defer api .websocketWaitGroup .Done ()
749
+
750
+ workspaceAgent := httpmw .WorkspaceAgent (r )
751
+ resource , err := api .Database .GetWorkspaceResourceByID (r .Context (), workspaceAgent .ResourceID )
752
+ if err != nil {
753
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
754
+ Message : "Failed to get workspace resource." ,
755
+ Detail : err .Error (),
756
+ })
757
+ return
758
+ }
759
+
760
+ build , err := api .Database .GetWorkspaceBuildByJobID (r .Context (), resource .JobID )
761
+ if err != nil {
762
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
763
+ Message : "Failed to get build." ,
764
+ Detail : err .Error (),
765
+ })
766
+ return
767
+ }
768
+
769
+ workspace , err := api .Database .GetWorkspaceByID (r .Context (), build .WorkspaceID )
770
+ if err != nil {
771
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
772
+ Message : "Failed to get workspace." ,
773
+ Detail : err .Error (),
774
+ })
775
+ return
776
+ }
777
+
778
+ conn , err := websocket .Accept (rw , r , & websocket.AcceptOptions {
779
+ CompressionMode : websocket .CompressionDisabled ,
780
+ })
781
+ if err != nil {
782
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
783
+ Message : "Failed to accept websocket." ,
784
+ Detail : err .Error (),
785
+ })
786
+ return
787
+ }
788
+ defer conn .Close (websocket .StatusAbnormalClosure , "" )
789
+
790
+ // Allow overriding the stat interval for debugging and testing purposes.
791
+ ctx := r .Context ()
792
+ timer := time .NewTicker (api .AgentStatsRefreshInterval )
793
+ var lastReport codersdk.AgentStatsReportResponse
794
+ for {
795
+ err := wsjson .Write (ctx , conn , codersdk.AgentStatsReportRequest {})
796
+ if err != nil {
797
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
798
+ Message : "Failed to write report request." ,
799
+ Detail : err .Error (),
800
+ })
801
+ return
802
+ }
803
+ var rep codersdk.AgentStatsReportResponse
804
+
805
+ err = wsjson .Read (ctx , conn , & rep )
806
+ if err != nil {
807
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
808
+ Message : "Failed to read report response." ,
809
+ Detail : err .Error (),
810
+ })
811
+ return
812
+ }
813
+
814
+ repJSON , err := json .Marshal (rep )
815
+ if err != nil {
816
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
817
+ Message : "Failed to marshal stat json." ,
818
+ Detail : err .Error (),
819
+ })
820
+ return
821
+ }
822
+
823
+ // Avoid inserting duplicate rows to preserve DB space.
824
+ var insert = ! reflect .DeepEqual (lastReport , rep )
825
+
826
+ api .Logger .Debug (ctx , "read stats report" ,
827
+ slog .F ("interval" , api .AgentStatsRefreshInterval ),
828
+ slog .F ("agent" , workspaceAgent .ID ),
829
+ slog .F ("resource" , resource .ID ),
830
+ slog .F ("workspace" , workspace .ID ),
831
+ slog .F ("insert" , insert ),
832
+ slog .F ("payload" , rep ),
833
+ )
834
+
835
+ if insert {
836
+ lastReport = rep
837
+
838
+ _ , err = api .Database .InsertAgentStat (ctx , database.InsertAgentStatParams {
839
+ ID : uuid .New (),
840
+ CreatedAt : time .Now (),
841
+ AgentID : workspaceAgent .ID ,
842
+ WorkspaceID : build .WorkspaceID ,
843
+ UserID : workspace .OwnerID ,
844
+ TemplateID : workspace .TemplateID ,
845
+ Payload : json .RawMessage (repJSON ),
846
+ })
847
+ if err != nil {
848
+ httpapi .Write (rw , http .StatusBadRequest , codersdk.Response {
849
+ Message : "Failed to insert agent stat." ,
850
+ Detail : err .Error (),
851
+ })
852
+ return
853
+ }
854
+ }
855
+
856
+ select {
857
+ case <- timer .C :
858
+ continue
859
+ case <- ctx .Done ():
860
+ conn .Close (websocket .StatusNormalClosure , "" )
861
+ return
862
+ }
863
+ }
864
+ }
742
865
743
866
// wsNetConn wraps net.Conn created by websocket.NetConn(). Cancel func
744
867
// is called if a read or write error is encountered.
0 commit comments