netbird/management/server/metrics/selfhosted.go
Yury Gargay 32880c56a4
Implement SQLite Store using gorm and relational approach (#1065)
Restructure data handling for improved performance and flexibility. 
Introduce 'G'-prefixed fields to represent Gorm relations, simplifying resource management. 
Eliminate complexity in lookup tables for enhanced query and write speed. 
Enable independent operations on data structures, requiring adjustments in the Store interface and Account Manager.
2023-10-12 15:42:36 +02:00

410 lines
11 KiB
Go

// Package metrics gather anonymous information about the usage of NetBird management
package metrics
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
"time"
"github.com/hashicorp/go-version"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/management/server"
nbversion "github.com/netbirdio/netbird/version"
)
const (
// PayloadEvent identifies an event type
PayloadEvent = "self-hosted stats"
// payloadEndpoint metrics defaultEndpoint to send anonymous data
payloadEndpoint = "https://metrics.netbird.io"
// defaultPushInterval default interval to push metrics
defaultPushInterval = 24 * time.Hour
// requestTimeout http request timeout
requestTimeout = 30 * time.Second
)
type getTokenResponse struct {
PublicAPIToken string `json:"public_api_token"`
}
type pushPayload struct {
APIKey string `json:"api_key"`
DistinctID string `json:"distinct_id"`
Event string `json:"event"`
Properties properties `json:"properties"`
Timestamp time.Time `json:"timestamp"`
}
// properties metrics to push
type properties map[string]interface{}
// DataSource metric data source
type DataSource interface {
GetAllAccounts() []*server.Account
GetStoreKind() server.StoreKind
}
// ConnManager peer connection manager that holds state for current active connections
type ConnManager interface {
GetAllConnectedPeers() map[string]struct{}
}
// Worker metrics collector and pusher
type Worker struct {
ctx context.Context
id string
idpManager string
dataSource DataSource
connManager ConnManager
startupTime time.Time
lastRun time.Time
}
// NewWorker returns a metrics worker
func NewWorker(ctx context.Context, id string, dataSource DataSource, connManager ConnManager, idpManager string) *Worker {
currentTime := time.Now()
return &Worker{
ctx: ctx,
id: id,
idpManager: idpManager,
dataSource: dataSource,
connManager: connManager,
startupTime: currentTime,
lastRun: currentTime,
}
}
// Run runs the metrics worker
func (w *Worker) Run() {
pushTicker := time.NewTicker(defaultPushInterval)
for {
select {
case <-w.ctx.Done():
return
case <-pushTicker.C:
err := w.sendMetrics()
if err != nil {
log.Error(err)
}
w.lastRun = time.Now()
}
}
}
func (w *Worker) sendMetrics() error {
ctx, cancel := context.WithTimeout(w.ctx, requestTimeout)
defer cancel()
apiKey, err := getAPIKey(ctx)
if err != nil {
return err
}
payload := w.generatePayload(apiKey)
payloadString, err := buildMetricsPayload(payload)
if err != nil {
return err
}
httpClient := http.Client{}
exportJobReq, err := createPostRequest(ctx, payloadEndpoint+"/capture/", payloadString)
if err != nil {
return fmt.Errorf("unable to create metrics post request %v", err)
}
jobResp, err := httpClient.Do(exportJobReq)
if err != nil {
return fmt.Errorf("unable to push metrics %v", err)
}
defer func() {
err = jobResp.Body.Close()
if err != nil {
log.Errorf("error while closing update metrics response body: %v", err)
}
}()
if jobResp.StatusCode != 200 {
return fmt.Errorf("unable to push anonymous metrics, got statusCode %d", jobResp.StatusCode)
}
log.Infof("sent anonymous metrics, next push will happen in %s. "+
"You can disable these metrics by running with flag --disable-anonymous-metrics,"+
" see more information at https://netbird.io/docs/FAQ/metrics-collection", defaultPushInterval)
return nil
}
func (w *Worker) generatePayload(apiKey string) pushPayload {
properties := w.generateProperties()
return pushPayload{
APIKey: apiKey,
DistinctID: w.id,
Event: PayloadEvent,
Properties: properties,
Timestamp: time.Now(),
}
}
func (w *Worker) generateProperties() properties {
var (
uptime float64
accounts int
expirationEnabled int
users int
serviceUsers int
pats int
peers int
peersSSHEnabled int
setupKeysUsage int
ephemeralPeersSKs int
ephemeralPeersSKUsage int
activePeersLastDay int
osPeers map[string]int
userPeers int
rules int
rulesProtocol map[string]int
rulesDirection map[string]int
groups int
routes int
routesWithRGGroups int
nameservers int
uiClient int
version string
peerActiveVersions []string
osUIClients map[string]int
)
start := time.Now()
metricsProperties := make(properties)
osPeers = make(map[string]int)
osUIClients = make(map[string]int)
rulesProtocol = make(map[string]int)
rulesDirection = make(map[string]int)
uptime = time.Since(w.startupTime).Seconds()
connections := w.connManager.GetAllConnectedPeers()
version = nbversion.NetbirdVersion()
for _, account := range w.dataSource.GetAllAccounts() {
accounts++
if account.Settings.PeerLoginExpirationEnabled {
expirationEnabled++
}
groups = groups + len(account.Groups)
routes = routes + len(account.Routes)
for _, route := range account.Routes {
if len(route.PeerGroups) > 0 {
routesWithRGGroups++
}
}
nameservers = nameservers + len(account.NameServerGroups)
for _, policy := range account.Policies {
for _, rule := range policy.Rules {
rules++
rulesProtocol[string(rule.Protocol)]++
if rule.Bidirectional {
rulesDirection["bidirectional"]++
} else {
rulesDirection["oneway"]++
}
}
}
for _, user := range account.Users {
if user.IsServiceUser {
serviceUsers++
} else {
users++
}
pats += len(user.PATs)
}
for _, key := range account.SetupKeys {
setupKeysUsage = setupKeysUsage + key.UsedTimes
if key.Ephemeral {
ephemeralPeersSKs++
ephemeralPeersSKUsage = ephemeralPeersSKUsage + key.UsedTimes
}
}
for _, peer := range account.Peers {
peers++
if peer.SSHEnabled {
peersSSHEnabled++
}
if peer.SetupKey == "" {
userPeers++
}
osKey := strings.ToLower(fmt.Sprintf("peer_os_%s", peer.Meta.GoOS))
osCount := osPeers[osKey]
osPeers[osKey] = osCount + 1
if peer.Meta.UIVersion != "" {
uiClient++
uiOSKey := strings.ToLower(fmt.Sprintf("ui_client_os_%s", peer.Meta.GoOS))
osUICount := osUIClients[uiOSKey]
osUIClients[uiOSKey] = osUICount + 1
}
_, connected := connections[peer.ID]
if connected || peer.Status.LastSeen.After(w.lastRun) {
activePeersLastDay++
osActiveKey := osKey + "_active"
osActiveCount := osPeers[osActiveKey]
osPeers[osActiveKey] = osActiveCount + 1
peerActiveVersions = append(peerActiveVersions, peer.Meta.WtVersion)
}
}
}
minActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions)
metricsProperties["uptime"] = uptime
metricsProperties["accounts"] = accounts
metricsProperties["users"] = users
metricsProperties["service_users"] = serviceUsers
metricsProperties["pats"] = pats
metricsProperties["peers"] = peers
metricsProperties["peers_ssh_enabled"] = peersSSHEnabled
metricsProperties["peers_login_expiration_enabled"] = expirationEnabled
metricsProperties["setup_keys_usage"] = setupKeysUsage
metricsProperties["ephemeral_peers_setup_keys"] = ephemeralPeersSKs
metricsProperties["ephemeral_peers_setup_keys_usage"] = ephemeralPeersSKUsage
metricsProperties["active_peers_last_day"] = activePeersLastDay
metricsProperties["user_peers"] = userPeers
metricsProperties["rules"] = rules
metricsProperties["groups"] = groups
metricsProperties["routes"] = routes
metricsProperties["routes_with_routing_groups"] = routesWithRGGroups
metricsProperties["nameservers"] = nameservers
metricsProperties["version"] = version
metricsProperties["min_active_peer_version"] = minActivePeerVersion
metricsProperties["max_active_peer_version"] = maxActivePeerVersion
metricsProperties["ui_clients"] = uiClient
metricsProperties["idp_manager"] = w.idpManager
metricsProperties["store_kind"] = w.dataSource.GetStoreKind()
for protocol, count := range rulesProtocol {
metricsProperties["rules_protocol_"+protocol] = count
}
for direction, count := range rulesDirection {
metricsProperties["rules_direction_"+direction] = count
}
for os, count := range osPeers {
metricsProperties[os] = count
}
for os, count := range osUIClients {
metricsProperties[os] = count
}
metricsProperties["metric_generation_time"] = time.Since(start).Milliseconds()
return metricsProperties
}
func getAPIKey(ctx context.Context) (string, error) {
httpClient := http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, payloadEndpoint+"/GetToken", nil)
if err != nil {
return "", fmt.Errorf("unable to create request for metrics public api token %v", err)
}
response, err := httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("unable to request metrics public api token %v", err)
}
defer func() {
err = response.Body.Close()
if err != nil {
log.Errorf("error while closing metrics token response body: %v", err)
}
}()
if response.StatusCode != 200 {
return "", fmt.Errorf("unable to retrieve metrics token, statusCode %d", response.StatusCode)
}
body, err := io.ReadAll(response.Body)
if err != nil {
return "", fmt.Errorf("coudln't get metrics token response; %v", err)
}
var tokenResponse getTokenResponse
err = json.Unmarshal(body, &tokenResponse)
if err != nil {
return "", fmt.Errorf("coudln't parse metrics public api token; %v", err)
}
return tokenResponse.PublicAPIToken, nil
}
func buildMetricsPayload(payload pushPayload) (string, error) {
str, err := json.Marshal(payload)
if err != nil {
return "", fmt.Errorf("unable to marshal metrics payload, got err: %v", err)
}
return string(str), nil
}
func createPostRequest(ctx context.Context, endpoint string, payloadStr string) (*http.Request, error) {
reqURL := endpoint
payload := strings.NewReader(payloadStr)
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, payload)
if err != nil {
return nil, err
}
req.Header.Add("content-type", "application/json")
return req, nil
}
func getMinMaxVersion(inputList []string) (string, string) {
reg, err := regexp.Compile(version.SemverRegexpRaw)
if err != nil {
return "", ""
}
versions := make([]*version.Version, 0)
for _, raw := range inputList {
if raw != "" && reg.MatchString(raw) {
v, err := version.NewVersion(raw)
if err == nil {
versions = append(versions, v)
}
}
}
switch len(versions) {
case 0:
return "", ""
case 1:
v := versions[0].String()
return v, v
default:
sort.Sort(version.Collection(versions))
return versions[0].String(), versions[len(versions)-1].String()
}
}