add metric endpoint to public API (#72, #80)

This commit is contained in:
Christoph Haas 2025-01-11 23:42:05 +01:00
parent 63d85d8123
commit 2d78fe33b8
7 changed files with 863 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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": {

View File

@ -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

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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
}