diff --git a/cmd/wg-portal/main.go b/cmd/wg-portal/main.go index da2876a..771a70c 100644 --- a/cmd/wg-portal/main.go +++ b/cmd/wg-portal/main.go @@ -109,16 +109,20 @@ func main() { apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager) apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager) apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager) + apiV1BackendMetrics := backendV1.NewMetricsService(cfg, database, userManager, wireGuardManager) apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers) apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers) apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces) apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning) + apiV1EndpointMetrics := handlersV1.NewMetricsEndpoint(apiV1BackendMetrics) + apiV1 := handlersV1.NewRestApi( userManager, apiV1EndpointUsers, apiV1EndpointPeers, apiV1EndpointInterfaces, apiV1EndpointProvisioning, + apiV1EndpointMetrics, ) webSrv, err := core.NewServer(cfg, apiFrontend, apiV1) diff --git a/internal/adapters/database.go b/internal/adapters/database.go index 98e4967..6e75f40 100644 --- a/internal/adapters/database.go +++ b/internal/adapters/database.go @@ -295,6 +295,30 @@ func (r *SqlRepo) GetAllInterfaces(ctx context.Context) ([]domain.Interface, err return interfaces, nil } +func (r *SqlRepo) GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) ( + *domain.InterfaceStatus, + error, +) { + if id == "" { + return nil, nil + } + + var stats []domain.InterfaceStatus + + err := r.db.WithContext(ctx).Where("identifier = ?", id).Find(&stats).Error + if err != nil { + return nil, err + } + + if len(stats) == 0 { + return nil, domain.ErrNotFound + } + + stat := stats[0] + + return &stat, nil +} + func (r *SqlRepo) FindInterfaces(ctx context.Context, search string) ([]domain.Interface, error) { var users []domain.Interface diff --git a/internal/app/api/core/assets/doc/v1_swagger.json b/internal/app/api/core/assets/doc/v1_swagger.json index d23abeb..80d01cd 100644 --- a/internal/app/api/core/assets/doc/v1_swagger.json +++ b/internal/app/api/core/assets/doc/v1_swagger.json @@ -309,6 +309,180 @@ } } }, + "/metrics/by-interface/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Metrics" + ], + "summary": "Get all metrics for a WireGuard Portal interface.", + "operationId": "metrics_handleMetricsForInterfaceGet", + "parameters": [ + { + "type": "string", + "description": "The WireGuard interface identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.InterfaceMetrics" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/metrics/by-peer/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Metrics" + ], + "summary": "Get all metrics for a WireGuard Portal peer.", + "operationId": "metrics_handleMetricsForPeerGet", + "parameters": [ + { + "type": "string", + "description": "The peer identifier (public key).", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PeerMetrics" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, + "/metrics/by-user/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Metrics" + ], + "summary": "Get all metrics for a WireGuard Portal user.", + "operationId": "metrics_handleMetricsForUserGet", + "parameters": [ + { + "type": "string", + "description": "The user identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserMetrics" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, "/peer/by-id/{id}": { "get": { "security": [ @@ -1523,6 +1697,26 @@ } } }, + "models.InterfaceMetrics": { + "type": "object", + "properties": { + "BytesReceived": { + "description": "The number of bytes received by the interface.", + "type": "integer", + "example": 123456789 + }, + "BytesTransmitted": { + "description": "The number of bytes transmitted by the interface.", + "type": "integer", + "example": 123456789 + }, + "InterfaceIdentifier": { + "description": "The unique identifier of the interface.", + "type": "string", + "example": "wg0" + } + } + }, "models.Peer": { "type": "object", "required": [ @@ -1728,6 +1922,51 @@ } } }, + "models.PeerMetrics": { + "type": "object", + "properties": { + "BytesReceived": { + "description": "The number of bytes received by the peer.", + "type": "integer", + "example": 123456789 + }, + "BytesTransmitted": { + "description": "The number of bytes transmitted by the peer.", + "type": "integer", + "example": 123456789 + }, + "Endpoint": { + "description": "The current endpoint address of the peer.", + "type": "string", + "example": "12.34.56.78" + }, + "IsPingable": { + "description": "If this field is set, the peer is pingable.", + "type": "boolean", + "example": true + }, + "LastHandshake": { + "description": "The last time the peer initiated a handshake.", + "type": "string", + "example": "2021-01-01T12:00:00Z" + }, + "LastPing": { + "description": "The last time the peer responded to a ICMP ping request.", + "type": "string", + "example": "2021-01-01T12:00:00Z" + }, + "LastSessionStart": { + "description": "The last time the peer initiated a session.", + "type": "string", + "example": "2021-01-01T12:00:00Z" + }, + "PeerIdentifier": { + "description": "The unique identifier of the peer.", + "type": "string", + "example": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" + } + } + }, "models.ProvisioningRequest": { "type": "object", "required": [ @@ -1922,6 +2161,38 @@ "example": true } } + }, + "models.UserMetrics": { + "type": "object", + "properties": { + "BytesReceived": { + "description": "The total number of bytes received by the user. This is the sum of all bytes received by the peers linked to the user.", + "type": "integer", + "example": 123456789 + }, + "BytesTransmitted": { + "description": "The total number of bytes transmitted by the user. This is the sum of all bytes transmitted by the peers linked to the user.", + "type": "integer", + "example": 123456789 + }, + "PeerCount": { + "description": "PeerCount represents the number of peers linked to the user.", + "type": "integer", + "example": 2 + }, + "PeerMetrics": { + "description": "PeerMetrics represents the metrics of the peers linked to the user.", + "type": "array", + "items": { + "$ref": "#/definitions/models.PeerMetrics" + } + }, + "UserIdentifier": { + "description": "The unique identifier of the user.", + "type": "string", + "example": "uid-1234567" + } + } } }, "securityDefinitions": { diff --git a/internal/app/api/core/assets/doc/v1_swagger.yaml b/internal/app/api/core/assets/doc/v1_swagger.yaml index 00c914f..2f092b7 100644 --- a/internal/app/api/core/assets/doc/v1_swagger.yaml +++ b/internal/app/api/core/assets/doc/v1_swagger.yaml @@ -239,6 +239,21 @@ definitions: - PrivateKey - PublicKey type: object + models.InterfaceMetrics: + properties: + BytesReceived: + description: The number of bytes received by the interface. + example: 123456789 + type: integer + BytesTransmitted: + description: The number of bytes transmitted by the interface. + example: 123456789 + type: integer + InterfaceIdentifier: + description: The unique identifier of the interface. + example: wg0 + type: string + type: object models.Peer: properties: Addresses: @@ -383,6 +398,41 @@ definitions: - InterfaceIdentifier - PrivateKey type: object + models.PeerMetrics: + properties: + BytesReceived: + description: The number of bytes received by the peer. + example: 123456789 + type: integer + BytesTransmitted: + description: The number of bytes transmitted by the peer. + example: 123456789 + type: integer + Endpoint: + description: The current endpoint address of the peer. + example: 12.34.56.78 + type: string + IsPingable: + description: If this field is set, the peer is pingable. + example: true + type: boolean + LastHandshake: + description: The last time the peer initiated a handshake. + example: "2021-01-01T12:00:00Z" + type: string + LastPing: + description: The last time the peer responded to a ICMP ping request. + example: "2021-01-01T12:00:00Z" + type: string + LastSessionStart: + description: The last time the peer initiated a session. + example: "2021-01-01T12:00:00Z" + type: string + PeerIdentifier: + description: The unique identifier of the peer. + example: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg= + type: string + type: object models.ProvisioningRequest: properties: InterfaceIdentifier: @@ -547,6 +597,33 @@ definitions: example: true type: boolean type: object + models.UserMetrics: + properties: + BytesReceived: + description: The total number of bytes received by the user. This is the sum + of all bytes received by the peers linked to the user. + example: 123456789 + type: integer + BytesTransmitted: + description: The total number of bytes transmitted by the user. This is the + sum of all bytes transmitted by the peers linked to the user. + example: 123456789 + type: integer + PeerCount: + description: PeerCount represents the number of peers linked to the user. + example: 2 + type: integer + PeerMetrics: + description: PeerMetrics represents the metrics of the peers linked to the + user. + items: + $ref: '#/definitions/models.PeerMetrics' + type: array + UserIdentifier: + description: The unique identifier of the user. + example: uid-1234567 + type: string + type: object info: contact: name: WireGuard Portal Project @@ -749,6 +826,117 @@ paths: summary: Create a new interface record. tags: - Interfaces + /metrics/by-interface/{id}: + get: + operationId: metrics_handleMetricsForInterfaceGet + parameters: + - description: The WireGuard interface identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.InterfaceMetrics' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all metrics for a WireGuard Portal interface. + tags: + - Metrics + /metrics/by-peer/{id}: + get: + operationId: metrics_handleMetricsForPeerGet + parameters: + - description: The peer identifier (public key). + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PeerMetrics' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all metrics for a WireGuard Portal peer. + tags: + - Metrics + /metrics/by-user/{id}: + get: + operationId: metrics_handleMetricsForUserGet + parameters: + - description: The user identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserMetrics' + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Error' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "404": + description: Not Found + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Get all metrics for a WireGuard Portal user. + tags: + - Metrics /peer/by-id/{id}: delete: operationId: peers_handleDelete diff --git a/internal/app/api/v1/backend/metrics_service.go b/internal/app/api/v1/backend/metrics_service.go new file mode 100644 index 0000000..010c8d7 --- /dev/null +++ b/internal/app/api/v1/backend/metrics_service.go @@ -0,0 +1,131 @@ +package backend + +import ( + "context" + "fmt" + + "github.com/h44z/wg-portal/internal/config" + "github.com/h44z/wg-portal/internal/domain" +) + +type MetricsServiceDatabaseRepo interface { + GetPeersStats(ctx context.Context, ids ...domain.PeerIdentifier) ([]domain.PeerStatus, error) + GetInterfaceStats(ctx context.Context, id domain.InterfaceIdentifier) ( + *domain.InterfaceStatus, + error, + ) + GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) +} + +type MetricsServiceUserManagerRepo interface { + GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) +} + +type MetricsServicePeerManagerRepo interface { + GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) +} + +type MetricsService struct { + cfg *config.Config + + db MetricsServiceDatabaseRepo + users MetricsServiceUserManagerRepo + peers MetricsServicePeerManagerRepo +} + +func NewMetricsService( + cfg *config.Config, + db MetricsServiceDatabaseRepo, + users MetricsServiceUserManagerRepo, + peers MetricsServicePeerManagerRepo, +) *MetricsService { + return &MetricsService{ + cfg: cfg, + db: db, + users: users, + peers: peers, + } +} + +func (m MetricsService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) ( + *domain.InterfaceStatus, + error, +) { + if !m.cfg.Statistics.CollectInterfaceData { + return nil, fmt.Errorf("interface statistics collection is disabled") + } + + // validate admin rights + if err := domain.ValidateAdminAccessRights(ctx); err != nil { + return nil, err + } + + interfaceStats, err := m.db.GetInterfaceStats(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to fetch stats for interface %s: %w", id, err) + } + + return interfaceStats, nil +} + +func (m MetricsService) GetForUser(ctx context.Context, id domain.UserIdentifier) ( + *domain.User, + []domain.PeerStatus, + error, +) { + if !m.cfg.Statistics.CollectPeerData { + return nil, nil, fmt.Errorf("statistics collection is disabled") + } + + if err := domain.ValidateUserAccessRights(ctx, id); err != nil { + return nil, nil, err + } + + user, err := m.users.GetUser(ctx, id) + if err != nil { + return nil, nil, err + } + + peers, err := m.db.GetUserPeers(ctx, user.Identifier) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch peers for user %s: %w", user.Identifier, err) + } + + peerIds := make([]domain.PeerIdentifier, len(peers)) + for i, peer := range peers { + peerIds[i] = peer.Identifier + } + + peerStats, err := m.db.GetPeersStats(ctx, peerIds...) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch peer stats for user %s: %w", user.Identifier, err) + } + + return user, peerStats, nil +} + +func (m MetricsService) GetForPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.PeerStatus, error) { + if !m.cfg.Statistics.CollectPeerData { + return nil, fmt.Errorf("peer statistics collection is disabled") + } + + peer, err := m.peers.GetPeer(ctx, id) + if err != nil { + return nil, err + } + + if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil { + return nil, err + } + + peerStats, err := m.db.GetPeersStats(ctx, peer.Identifier) + if err != nil { + return nil, fmt.Errorf("failed to fetch stats for peer %s: %w", peer.Identifier, err) + } + + if len(peerStats) == 0 { + return nil, fmt.Errorf("no stats found for peer %s: %w", peer.Identifier, domain.ErrNotFound) + } + + return &peerStats[0], nil +} diff --git a/internal/app/api/v1/handlers/endpoint_metrics.go b/internal/app/api/v1/handlers/endpoint_metrics.go new file mode 100644 index 0000000..4890c76 --- /dev/null +++ b/internal/app/api/v1/handlers/endpoint_metrics.go @@ -0,0 +1,140 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/h44z/wg-portal/internal/app/api/v1/models" + "github.com/h44z/wg-portal/internal/domain" +) + +type MetricsEndpointStatisticsService interface { + GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.InterfaceStatus, error) + GetForUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, []domain.PeerStatus, error) + GetForPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.PeerStatus, error) +} + +type MetricsEndpoint struct { + metrics MetricsEndpointStatisticsService +} + +func NewMetricsEndpoint(metrics MetricsEndpointStatisticsService) *MetricsEndpoint { + return &MetricsEndpoint{ + metrics: metrics, + } +} + +func (e MetricsEndpoint) GetName() string { + return "MetricsEndpoint" +} + +func (e MetricsEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) { + apiGroup := g.Group("/metrics", authenticator.LoggedIn()) + + apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleMetricsForInterfaceGet()) + apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleMetricsForUserGet()) + apiGroup.GET("/by-peer/:id", authenticator.LoggedIn(), e.handleMetricsForPeerGet()) +} + +// handleMetricsForInterfaceGet returns a gorm Handler function. +// +// @ID metrics_handleMetricsForInterfaceGet +// @Tags Metrics +// @Summary Get all metrics for a WireGuard Portal interface. +// @Param id path string true "The WireGuard interface identifier." +// @Produce json +// @Success 200 {object} models.InterfaceMetrics +// @Failure 400 {object} models.Error +// @Failure 401 {object} models.Error +// @Failure 404 {object} models.Error +// @Failure 500 {object} models.Error +// @Router /metrics/by-interface/{id} [get] +// @Security BasicAuth +func (e MetricsEndpoint) handleMetricsForInterfaceGet() gin.HandlerFunc { + return func(c *gin.Context) { + ctx := domain.SetUserInfoFromGin(c) + + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"}) + return + } + + interfaceMetrics, err := e.metrics.GetForInterface(ctx, domain.InterfaceIdentifier(id)) + if err != nil { + c.JSON(ParseServiceError(err)) + return + } + + c.JSON(http.StatusOK, models.NewInterfaceMetrics(interfaceMetrics)) + } +} + +// handleMetricsForUserGet returns a gorm Handler function. +// +// @ID metrics_handleMetricsForUserGet +// @Tags Metrics +// @Summary Get all metrics for a WireGuard Portal user. +// @Param id path string true "The user identifier." +// @Produce json +// @Success 200 {object} models.UserMetrics +// @Failure 400 {object} models.Error +// @Failure 401 {object} models.Error +// @Failure 404 {object} models.Error +// @Failure 500 {object} models.Error +// @Router /metrics/by-user/{id} [get] +// @Security BasicAuth +func (e MetricsEndpoint) handleMetricsForUserGet() gin.HandlerFunc { + return func(c *gin.Context) { + ctx := domain.SetUserInfoFromGin(c) + + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"}) + return + } + + user, userMetrics, err := e.metrics.GetForUser(ctx, domain.UserIdentifier(id)) + if err != nil { + c.JSON(ParseServiceError(err)) + return + } + + c.JSON(http.StatusOK, models.NewUserMetrics(user, userMetrics)) + } +} + +// handleMetricsForPeerGet returns a gorm Handler function. +// +// @ID metrics_handleMetricsForPeerGet +// @Tags Metrics +// @Summary Get all metrics for a WireGuard Portal peer. +// @Param id path string true "The peer identifier (public key)." +// @Produce json +// @Success 200 {object} models.PeerMetrics +// @Failure 400 {object} models.Error +// @Failure 401 {object} models.Error +// @Failure 404 {object} models.Error +// @Failure 500 {object} models.Error +// @Router /metrics/by-peer/{id} [get] +// @Security BasicAuth +func (e MetricsEndpoint) handleMetricsForPeerGet() gin.HandlerFunc { + return func(c *gin.Context) { + ctx := domain.SetUserInfoFromGin(c) + + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"}) + return + } + + peerMetrics, err := e.metrics.GetForPeer(ctx, domain.PeerIdentifier(id)) + if err != nil { + c.JSON(ParseServiceError(err)) + return + } + + c.JSON(http.StatusOK, models.NewPeerMetrics(peerMetrics)) + } +} diff --git a/internal/app/api/v1/models/models_metrics.go b/internal/app/api/v1/models/models_metrics.go new file mode 100644 index 0000000..163d8e8 --- /dev/null +++ b/internal/app/api/v1/models/models_metrics.go @@ -0,0 +1,105 @@ +package models + +import ( + "time" + + "github.com/h44z/wg-portal/internal/domain" +) + +// PeerMetrics represents the metrics of a WireGuard peer. +type PeerMetrics struct { + // The unique identifier of the peer. + PeerIdentifier string `json:"PeerIdentifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="` + + // If this field is set, the peer is pingable. + IsPingable bool `json:"IsPingable" example:"true"` + // The last time the peer responded to a ICMP ping request. + LastPing *time.Time `json:"LastPing" example:"2021-01-01T12:00:00Z"` + + // The number of bytes received by the peer. + BytesReceived uint64 `json:"BytesReceived" example:"123456789"` + // The number of bytes transmitted by the peer. + BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"` + + // The last time the peer initiated a handshake. + LastHandshake *time.Time `json:"LastHandshake" example:"2021-01-01T12:00:00Z"` + // The current endpoint address of the peer. + Endpoint string `json:"Endpoint" example:"12.34.56.78"` + // The last time the peer initiated a session. + LastSessionStart *time.Time `json:"LastSessionStart" example:"2021-01-01T12:00:00Z"` +} + +func NewPeerMetrics(src *domain.PeerStatus) *PeerMetrics { + return &PeerMetrics{ + PeerIdentifier: string(src.PeerId), + IsPingable: src.IsPingable, + LastPing: src.LastPing, + BytesReceived: src.BytesReceived, + BytesTransmitted: src.BytesTransmitted, + LastHandshake: src.LastHandshake, + Endpoint: src.Endpoint, + LastSessionStart: src.LastSessionStart, + } +} + +// InterfaceMetrics represents the metrics of a WireGuard interface. +type InterfaceMetrics struct { + // The unique identifier of the interface. + InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"` + + // The number of bytes received by the interface. + BytesReceived uint64 `json:"BytesReceived" example:"123456789"` + // The number of bytes transmitted by the interface. + BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"` +} + +func NewInterfaceMetrics(src *domain.InterfaceStatus) *InterfaceMetrics { + return &InterfaceMetrics{ + InterfaceIdentifier: string(src.InterfaceId), + BytesReceived: src.BytesReceived, + BytesTransmitted: src.BytesTransmitted, + } +} + +// UserMetrics represents the metrics of a WireGuard user. +type UserMetrics struct { + // The unique identifier of the user. + UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"` + + // PeerCount represents the number of peers linked to the user. + PeerCount int `json:"PeerCount" example:"2"` + + // The total number of bytes received by the user. This is the sum of all bytes received by the peers linked to the user. + BytesReceived uint64 `json:"BytesReceived" example:"123456789"` + // The total number of bytes transmitted by the user. This is the sum of all bytes transmitted by the peers linked to the user. + BytesTransmitted uint64 `json:"BytesTransmitted" example:"123456789"` + + // PeerMetrics represents the metrics of the peers linked to the user. + PeerMetrics []PeerMetrics `json:"PeerMetrics"` +} + +func NewUserMetrics(srcUser *domain.User, src []domain.PeerStatus) *UserMetrics { + if srcUser == nil { + return nil + } + + um := &UserMetrics{ + UserIdentifier: string(srcUser.Identifier), + PeerCount: srcUser.LinkedPeerCount, + PeerMetrics: []PeerMetrics{}, + + BytesReceived: 0, + BytesTransmitted: 0, + } + + peerMetrics := make([]PeerMetrics, len(src)) + for i, peer := range src { + peerMetrics[i] = *NewPeerMetrics(&peer) + + um.BytesReceived += peer.BytesReceived + um.BytesTransmitted += peer.BytesTransmitted + } + um.PeerMetrics = peerMetrics + + return um +}