Skip to content

Commit a84c607

Browse files
authored
(BEDS-461) implement get user machine metrics (#889)
1 parent 115bf7f commit a84c607

File tree

10 files changed

+273
-23
lines changed

10 files changed

+273
-23
lines changed

backend/pkg/api/data_access/data_access.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type DataAccessor interface {
3030
ProtocolRepository
3131
RatelimitRepository
3232
HealthzRepository
33+
MachineRepository
3334

3435
StartDataAccessServices()
3536
Close()

backend/pkg/api/data_access/dummy.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import (
66
"fmt"
77
"math/rand/v2"
88
"reflect"
9+
"slices"
910
"time"
1011

1112
"github.com/go-faker/faker/v4"
1213
"github.com/go-faker/faker/v4/pkg/options"
1314
"github.com/gobitfly/beaconchain/pkg/api/enums"
15+
"github.com/gobitfly/beaconchain/pkg/api/types"
1416
t "github.com/gobitfly/beaconchain/pkg/api/types"
1517
"github.com/gobitfly/beaconchain/pkg/userservice"
1618
"github.com/shopspring/decimal"
@@ -652,3 +654,20 @@ func (d *DummyService) IncrementBundleDeliveryCount(ctx context.Context, bundleV
652654
func (d *DummyService) GetValidatorDashboardMobileWidget(ctx context.Context, dashboardId t.VDBIdPrimary) (*t.MobileWidgetData, error) {
653655
return getDummyStruct[t.MobileWidgetData]()
654656
}
657+
658+
func (d *DummyService) GetUserMachineMetrics(ctx context.Context, userID uint64, limit uint64, offset uint64) (*types.MachineMetricsData, error) {
659+
data, err := getDummyStruct[types.MachineMetricsData]()
660+
if err != nil {
661+
return nil, err
662+
}
663+
data.SystemMetrics = slices.SortedFunc(slices.Values(data.SystemMetrics), func(i, j *t.MachineMetricSystem) int {
664+
return int(i.Timestamp) - int(j.Timestamp)
665+
})
666+
data.ValidatorMetrics = slices.SortedFunc(slices.Values(data.ValidatorMetrics), func(i, j *t.MachineMetricValidator) int {
667+
return int(i.Timestamp) - int(j.Timestamp)
668+
})
669+
data.NodeMetrics = slices.SortedFunc(slices.Values(data.NodeMetrics), func(i, j *t.MachineMetricNode) int {
670+
return int(i.Timestamp) - int(j.Timestamp)
671+
})
672+
return data, nil
673+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dataaccess
2+
3+
import (
4+
"context"
5+
6+
"github.com/gobitfly/beaconchain/pkg/api/types"
7+
)
8+
9+
type MachineRepository interface {
10+
GetUserMachineMetrics(context context.Context, userID uint64, limit uint64, offset uint64) (*types.MachineMetricsData, error)
11+
}
12+
13+
func (d *DataAccessService) GetUserMachineMetrics(ctx context.Context, userID uint64, limit uint64, offset uint64) (*types.MachineMetricsData, error) {
14+
return d.dummy.GetUserMachineMetrics(ctx, userID, limit, offset)
15+
}

backend/pkg/api/handlers/auth.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"cmp"
45
"context"
56
"errors"
67
"fmt"
@@ -181,13 +182,15 @@ const authHeaderPrefix = "Bearer "
181182

182183
func (h *HandlerService) GetUserIdByApiKey(r *http.Request) (uint64, error) {
183184
// TODO: store user id in context during ratelimting and use it here
184-
var apiKey string
185-
authHeader := r.Header.Get("Authorization")
186-
if strings.HasPrefix(authHeader, authHeaderPrefix) {
187-
apiKey = strings.TrimPrefix(authHeader, authHeaderPrefix)
188-
} else {
189-
apiKey = r.URL.Query().Get("api_key")
190-
}
185+
query := r.URL.Query()
186+
header := r.Header
187+
apiKey := cmp.Or(
188+
strings.TrimPrefix(header.Get("Authorization"), authHeaderPrefix),
189+
header.Get("X-Api-Key"),
190+
query.Get("api_key"),
191+
query.Get("apiKey"),
192+
query.Get("apikey"),
193+
)
191194
if apiKey == "" {
192195
return 0, newUnauthorizedErr("missing api key")
193196
}

backend/pkg/api/handlers/common.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ import (
2929
)
3030

3131
type HandlerService struct {
32-
dai dataaccess.DataAccessor
33-
scs *scs.SessionManager
32+
dai dataaccess.DataAccessor
33+
scs *scs.SessionManager
34+
isPostMachineMetricsEnabled bool // if more config options are needed, consider having the whole config in here
3435
}
3536

36-
func NewHandlerService(dataAccessor dataaccess.DataAccessor, sessionManager *scs.SessionManager) *HandlerService {
37+
func NewHandlerService(dataAccessor dataaccess.DataAccessor, sessionManager *scs.SessionManager, enablePostMachineMetrics bool) *HandlerService {
3738
if allNetworks == nil {
3839
networks, err := dataAccessor.GetAllNetworks()
3940
if err != nil {
@@ -43,8 +44,9 @@ func NewHandlerService(dataAccessor dataaccess.DataAccessor, sessionManager *scs
4344
}
4445

4546
return &HandlerService{
46-
dai: dataAccessor,
47-
scs: sessionManager,
47+
dai: dataAccessor,
48+
scs: sessionManager,
49+
isPostMachineMetricsEnabled: enablePostMachineMetrics,
4850
}
4951
}
5052

@@ -525,14 +527,10 @@ func checkEnum[T enums.EnumFactory[T]](v *validationError, enumString string, na
525527
return enum
526528
}
527529

528-
// checkEnumIsAllowed checks if the given enum is in the list of allowed enums.
529-
func checkEnumIsAllowed[T enums.EnumFactory[T]](v *validationError, enum T, allowed []T, name string) {
530-
if enums.IsInvalidEnum(enum) {
531-
v.add(name, "parameter is missing or invalid, please check the API documentation")
532-
return
533-
}
530+
// better func name would be
531+
func checkValueInAllowed[T cmp.Ordered](v *validationError, value T, allowed []T, name string) {
534532
for _, a := range allowed {
535-
if enum.Int() == a.Int() {
533+
if cmp.Compare(value, a) == 0 {
536534
return
537535
}
538536
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package handlers
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/gobitfly/beaconchain/pkg/api/types"
7+
)
8+
9+
func (h *HandlerService) InternalGetUserMachineMetrics(w http.ResponseWriter, r *http.Request) {
10+
h.PublicGetUserMachineMetrics(w, r)
11+
}
12+
13+
func (h *HandlerService) PublicGetUserMachineMetrics(w http.ResponseWriter, r *http.Request) {
14+
var v validationError
15+
userId, err := GetUserIdByContext(r)
16+
if err != nil {
17+
handleErr(w, r, err)
18+
return
19+
}
20+
q := r.URL.Query()
21+
offset := v.checkUint(q.Get("offset"), "offset")
22+
limit := uint64(180)
23+
if limitParam := q.Get("limit"); limitParam != "" {
24+
limit = v.checkUint(limitParam, "limit")
25+
}
26+
27+
data, err := h.dai.GetUserMachineMetrics(r.Context(), userId, limit, offset)
28+
if err != nil {
29+
handleErr(w, r, err)
30+
return
31+
}
32+
response := types.GetUserMachineMetricsRespone{
33+
Data: *data,
34+
}
35+
36+
returnOk(w, r, response)
37+
}

backend/pkg/api/handlers/public.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ func (h *HandlerService) PublicGetValidatorDashboardSummary(w http.ResponseWrite
10281028

10291029
period := checkEnum[enums.TimePeriod](&v, q.Get("period"), "period")
10301030
// allowed periods are: all_time, last_30d, last_7d, last_24h, last_1h
1031-
checkEnumIsAllowed(&v, period, summaryAllowedPeriods, "period")
1031+
checkValueInAllowed(&v, period, summaryAllowedPeriods, "period")
10321032
if v.hasErrors() {
10331033
handleErr(w, r, v)
10341034
return
@@ -1075,7 +1075,7 @@ func (h *HandlerService) PublicGetValidatorDashboardGroupSummary(w http.Response
10751075
groupId := v.checkGroupId(vars["group_id"], forbidEmpty)
10761076
period := checkEnum[enums.TimePeriod](&v, r.URL.Query().Get("period"), "period")
10771077
// allowed periods are: all_time, last_30d, last_7d, last_24h, last_1h
1078-
checkEnumIsAllowed(&v, period, summaryAllowedPeriods, "period")
1078+
checkValueInAllowed(&v, period, summaryAllowedPeriods, "period")
10791079
if v.hasErrors() {
10801080
handleErr(w, r, v)
10811081
return
@@ -1170,7 +1170,7 @@ func (h *HandlerService) PublicGetValidatorDashboardSummaryValidators(w http.Res
11701170
period := checkEnum[enums.TimePeriod](&v, q.Get("period"), "period")
11711171
// allowed periods are: all_time, last_30d, last_7d, last_24h, last_1h
11721172
allowedPeriods := []enums.TimePeriod{enums.TimePeriods.AllTime, enums.TimePeriods.Last30d, enums.TimePeriods.Last7d, enums.TimePeriods.Last24h, enums.TimePeriods.Last1h}
1173-
checkEnumIsAllowed(&v, period, allowedPeriods, "period")
1173+
checkValueInAllowed(&v, period, allowedPeriods, "period")
11741174
if v.hasErrors() {
11751175
handleErr(w, r, v)
11761176
return

backend/pkg/api/router.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func NewApiRouter(dataAccessor dataaccess.DataAccessor, cfg *types.Config) *mux.
3232
if !(cfg.Frontend.CsrfInsecure || cfg.Frontend.Debug) {
3333
internalRouter.Use(getCsrfProtectionMiddleware(cfg), csrfInjecterMiddleware)
3434
}
35-
handlerService := handlers.NewHandlerService(dataAccessor, sessionManager)
35+
handlerService := handlers.NewHandlerService(dataAccessor, sessionManager, !cfg.Frontend.DisableStatsInserts)
3636

3737
// store user id in context, if available
3838
publicRouter.Use(handlers.GetUserIdStoreMiddleware(handlerService.GetUserIdByApiKey))
@@ -123,6 +123,8 @@ func addRoutes(hs *handlers.HandlerService, publicRouter, internalRouter *mux.Ro
123123
{http.MethodGet, "/users/me/dashboards", hs.PublicGetUserDashboards, hs.InternalGetUserDashboards},
124124
{http.MethodPut, "/users/me/notifications/settings/paired-devices/{client_id}/token", nil, hs.InternalPostUsersMeNotificationSettingsPairedDevicesToken},
125125

126+
{http.MethodGet, "/users/me/machine-metrics", hs.PublicGetUserMachineMetrics, hs.InternalGetUserMachineMetrics},
127+
126128
{http.MethodPost, "/search", nil, hs.InternalPostSearch},
127129

128130
{http.MethodPost, "/account-dashboards", hs.PublicPostAccountDashboards, hs.InternalPostAccountDashboards},
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package types
2+
3+
type MachineMetricSystem struct {
4+
Timestamp uint64 `json:"timestamp,omitempty" faker:"boundary_start=1725166800, boundary_end=1725177600"`
5+
ExporterVersion string `json:"exporter_version,omitempty"`
6+
// system
7+
CpuCores uint64 `json:"cpu_cores,omitempty"`
8+
CpuThreads uint64 `json:"cpu_threads,omitempty"`
9+
CpuNodeSystemSecondsTotal uint64 `json:"cpu_node_system_seconds_total,omitempty"`
10+
CpuNodeUserSecondsTotal uint64 `json:"cpu_node_user_seconds_total,omitempty"`
11+
CpuNodeIowaitSecondsTotal uint64 `json:"cpu_node_iowait_seconds_total,omitempty"`
12+
CpuNodeIdleSecondsTotal uint64 `json:"cpu_node_idle_seconds_total,omitempty"`
13+
MemoryNodeBytesTotal uint64 `json:"memory_node_bytes_total,omitempty"`
14+
MemoryNodeBytesFree uint64 `json:"memory_node_bytes_free,omitempty"`
15+
MemoryNodeBytesCached uint64 `json:"memory_node_bytes_cached,omitempty"`
16+
MemoryNodeBytesBuffers uint64 `json:"memory_node_bytes_buffers,omitempty"`
17+
DiskNodeBytesTotal uint64 `json:"disk_node_bytes_total,omitempty"`
18+
DiskNodeBytesFree uint64 `json:"disk_node_bytes_free,omitempty"`
19+
DiskNodeIoSeconds uint64 `json:"disk_node_io_seconds,omitempty"`
20+
DiskNodeReadsTotal uint64 `json:"disk_node_reads_total,omitempty"`
21+
DiskNodeWritesTotal uint64 `json:"disk_node_writes_total,omitempty"`
22+
NetworkNodeBytesTotalReceive uint64 `json:"network_node_bytes_total_receive,omitempty"`
23+
NetworkNodeBytesTotalTransmit uint64 `json:"network_node_bytes_total_transmit,omitempty"`
24+
MiscNodeBootTsSeconds uint64 `json:"misc_node_boot_ts_seconds,omitempty"`
25+
MiscOs string `json:"misc_os,omitempty"`
26+
// do not store in bigtable but include them in generated model
27+
Machine *string `json:"machine,omitempty"`
28+
}
29+
30+
type MachineMetricValidator struct {
31+
Timestamp uint64 `json:"timestamp,omitempty" faker:"boundary_start=1725166800, boundary_end=1725177600"`
32+
ExporterVersion string `json:"exporter_version,omitempty"`
33+
// process
34+
CpuProcessSecondsTotal uint64 `json:"cpu_process_seconds_total,omitempty"`
35+
MemoryProcessBytes uint64 `json:"memory_process_bytes,omitempty"`
36+
ClientName string `json:"client_name,omitempty"`
37+
ClientVersion string `json:"client_version,omitempty"`
38+
ClientBuild uint64 `json:"client_build,omitempty"`
39+
SyncEth2FallbackConfigured bool `json:"sync_eth2_fallback_configured,omitempty"`
40+
SyncEth2FallbackConnected bool `json:"sync_eth2_fallback_connected,omitempty"`
41+
// validator
42+
ValidatorTotal uint64 `json:"validator_total,omitempty"`
43+
ValidatorActive uint64 `json:"validator_active,omitempty"`
44+
// do not store in bigtable but include them in generated model
45+
Machine *string `json:"machine,omitempty"`
46+
}
47+
48+
type MachineMetricNode struct {
49+
Timestamp uint64 `json:"timestamp,omitempty" faker:"boundary_start=1725166800, boundary_end=1725177600"`
50+
ExporterVersion string `json:"exporter_version,omitempty"`
51+
// process
52+
CpuProcessSecondsTotal uint64 `json:"cpu_process_seconds_total,omitempty"`
53+
MemoryProcessBytes uint64 `json:"memory_process_bytes,omitempty"`
54+
ClientName string `json:"client_name,omitempty"`
55+
ClientVersion string `json:"client_version,omitempty"`
56+
ClientBuild uint64 `json:"client_build,omitempty"`
57+
SyncEth2FallbackConfigured bool `json:"sync_eth2_fallback_configured,omitempty"`
58+
SyncEth2FallbackConnected bool `json:"sync_eth2_fallback_connected,omitempty"`
59+
// node
60+
DiskBeaconchainBytesTotal uint64 `json:"disk_beaconchain_bytes_total,omitempty"`
61+
NetworkLibp2PBytesTotalReceive uint64 `json:"network_libp2p_bytes_total_receive,omitempty"`
62+
NetworkLibp2PBytesTotalTransmit uint64 `json:"network_libp2p_bytes_total_transmit,omitempty"`
63+
NetworkPeersConnected uint64 `json:"network_peers_connected,omitempty"`
64+
SyncEth1Connected bool `json:"sync_eth1_connected,omitempty"`
65+
SyncEth2Synced bool `json:"sync_eth2_synced,omitempty"`
66+
SyncBeaconHeadSlot uint64 `json:"sync_beacon_head_slot,omitempty"`
67+
SyncEth1FallbackConfigured bool `json:"sync_eth1_fallback_configured,omitempty"`
68+
SyncEth1FallbackConnected bool `json:"sync_eth1_fallback_connected,omitempty"`
69+
// do not store in bigtable but include them in generated model
70+
Machine *string `json:"machine,omitempty"`
71+
}
72+
73+
type MachineMetricsData struct {
74+
SystemMetrics []*MachineMetricSystem `json:"system_metrics" faker:"slice_len=30"`
75+
ValidatorMetrics []*MachineMetricValidator `json:"validator_metrics" faker:"slice_len=30"`
76+
NodeMetrics []*MachineMetricNode `json:"node_metrics" faker:"slice_len=30"`
77+
}
78+
79+
type GetUserMachineMetricsRespone ApiDataResponse[MachineMetricsData]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Code generated by tygo. DO NOT EDIT.
2+
/* eslint-disable */
3+
import type { ApiDataResponse } from './common'
4+
5+
//////////
6+
// source: machine_metrics.go
7+
8+
export interface MachineMetricSystem {
9+
timestamp?: number /* uint64 */;
10+
exporter_version?: string;
11+
/**
12+
* system
13+
*/
14+
cpu_cores?: number /* uint64 */;
15+
cpu_threads?: number /* uint64 */;
16+
cpu_node_system_seconds_total?: number /* uint64 */;
17+
cpu_node_user_seconds_total?: number /* uint64 */;
18+
cpu_node_iowait_seconds_total?: number /* uint64 */;
19+
cpu_node_idle_seconds_total?: number /* uint64 */;
20+
memory_node_bytes_total?: number /* uint64 */;
21+
memory_node_bytes_free?: number /* uint64 */;
22+
memory_node_bytes_cached?: number /* uint64 */;
23+
memory_node_bytes_buffers?: number /* uint64 */;
24+
disk_node_bytes_total?: number /* uint64 */;
25+
disk_node_bytes_free?: number /* uint64 */;
26+
disk_node_io_seconds?: number /* uint64 */;
27+
disk_node_reads_total?: number /* uint64 */;
28+
disk_node_writes_total?: number /* uint64 */;
29+
network_node_bytes_total_receive?: number /* uint64 */;
30+
network_node_bytes_total_transmit?: number /* uint64 */;
31+
misc_node_boot_ts_seconds?: number /* uint64 */;
32+
misc_os?: string;
33+
/**
34+
* do not store in bigtable but include them in generated model
35+
*/
36+
machine?: string;
37+
}
38+
export interface MachineMetricValidator {
39+
timestamp?: number /* uint64 */;
40+
exporter_version?: string;
41+
/**
42+
* process
43+
*/
44+
cpu_process_seconds_total?: number /* uint64 */;
45+
memory_process_bytes?: number /* uint64 */;
46+
client_name?: string;
47+
client_version?: string;
48+
client_build?: number /* uint64 */;
49+
sync_eth2_fallback_configured?: boolean;
50+
sync_eth2_fallback_connected?: boolean;
51+
/**
52+
* validator
53+
*/
54+
validator_total?: number /* uint64 */;
55+
validator_active?: number /* uint64 */;
56+
/**
57+
* do not store in bigtable but include them in generated model
58+
*/
59+
machine?: string;
60+
}
61+
export interface MachineMetricNode {
62+
timestamp?: number /* uint64 */;
63+
exporter_version?: string;
64+
/**
65+
* process
66+
*/
67+
cpu_process_seconds_total?: number /* uint64 */;
68+
memory_process_bytes?: number /* uint64 */;
69+
client_name?: string;
70+
client_version?: string;
71+
client_build?: number /* uint64 */;
72+
sync_eth2_fallback_configured?: boolean;
73+
sync_eth2_fallback_connected?: boolean;
74+
/**
75+
* node
76+
*/
77+
disk_beaconchain_bytes_total?: number /* uint64 */;
78+
network_libp2p_bytes_total_receive?: number /* uint64 */;
79+
network_libp2p_bytes_total_transmit?: number /* uint64 */;
80+
network_peers_connected?: number /* uint64 */;
81+
sync_eth1_connected?: boolean;
82+
sync_eth2_synced?: boolean;
83+
sync_beacon_head_slot?: number /* uint64 */;
84+
sync_eth1_fallback_configured?: boolean;
85+
sync_eth1_fallback_connected?: boolean;
86+
/**
87+
* do not store in bigtable but include them in generated model
88+
*/
89+
machine?: string;
90+
}
91+
export interface MachineMetricsData {
92+
system_metrics: (MachineMetricSystem | undefined)[];
93+
validator_metrics: (MachineMetricValidator | undefined)[];
94+
node_metrics: (MachineMetricNode | undefined)[];
95+
}
96+
export type GetUserMachineMetricsRespone = ApiDataResponse<MachineMetricsData>;

0 commit comments

Comments
 (0)