mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-26 01:53:42 +01:00
32880c56a4
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.
410 lines
11 KiB
Go
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()
|
|
}
|
|
}
|