Skip to content

Commit 91f0d5c

Browse files
pappzlixmal
andauthored
[client] Feature/client metrics (#5512)
* Add client metrics * Add client metrics system with OpenTelemetry and VictoriaMetrics support Implements a comprehensive client metrics system to track peer connection stages and performance. The system supports multiple backend implementations (OpenTelemetry, VictoriaMetrics, and no-op) and tracks detailed connection stage durations from creation through WireGuard handshake. Key changes: - Add metrics package with pluggable backend implementations - Implement OpenTelemetry metrics backend - Implement VictoriaMetrics metrics backend - Add no-op metrics implementation for disabled state - Track connection stages: creation, semaphore, signaling, connection ready, and WireGuard handshake - Move WireGuard watcher functionality to conn.go - Refactor engine to integrate metrics tracking - Add metrics export endpoint in debug server * Add signaling metrics tracking for initial and reconnection attempts * Reset connection stage timestamps during reconnections to exclude unnecessary metrics tracking * Delete otel lib from client * Update unit tests * Invoke callback on handshake success in WireGuard watcher * Add Netbird version tracking to client metrics Integrate Netbird version into VictoriaMetrics backend and metrics labels. Update `ClientMetrics` constructor and metric name formatting to include version information. * Add sync duration tracking to client metrics Introduce `RecordSyncDuration` for measuring sync message processing time. Update all metrics implementations (VictoriaMetrics, no-op) to support the new method. Refactor `ClientMetrics` to use `AgentInfo` for static agent data. * Remove no-op metrics implementation and simplify ClientMetrics constructor Eliminate unused `noopMetrics` and refactor `ClientMetrics` to always use the VictoriaMetrics implementation. Update associated logic to reflect these changes. * Add total duration tracking for connection attempts Calculate total duration for both initial connections and reconnections, accounting for different timestamp scenarios. Update `Export` method to include Prometheus HELP comments. * Add metrics push support to VictoriaMetrics integration * [client] anchor connection metrics to first signal received * Remove creation_to_semaphore connection stage metric The semaphore queuing stage (Created → SemaphoreAcquired) is no longer tracked. Connection metrics now start from SignalingReceived. Updated docs and Grafana dashboard accordingly. * [client] Add remote push config for metrics with version-based eligibility Introduce remoteconfig.Manager that fetches a remote JSON config to control metrics push interval and restrict pushing to a specific agent version range. When NB_METRICS_INTERVAL is set, remote config is bypassed entirely for local override. * [client] Add WASM-compatible NewClientMetrics implementation Replace NewClientMetrics in metrics.go with a WASM-specific stub in metrics_js.go, returning nil for compatibility with JS builds. Simplify method usage for WASM targets. * Add missing file * Update default case in DeploymentType.String to return "unknown" instead of "selfhosted" * [client] Rework metrics to use timestamped samples instead of histograms Replace cumulative Prometheus histograms with timestamped point-in-time samples that are pushed once and cleared. This fixes metrics for sparse events (connections/syncs that happen once at startup) where rate() and increase() produced incorrect or empty results. Changes: - Switch from VictoriaMetrics histogram library to raw Prometheus text format with explicit millisecond timestamps - Reset samples after successful push (no resending stale data) - Rename connection_to_handshake → connection_to_wg_handshake - Add netbird_peer_connection_count metric for ICE vs Relay tracking - Simplify dashboard: point-based scatter plots, donut pie chart - Add maxStalenessInterval=1m to VictoriaMetrics to prevent forward-fill - Fix deployment_type Unknown returning "selfhosted" instead of "unknown" - Fix inverted shouldPush condition in push.go * [client] Add InfluxDB metrics backend alongside VictoriaMetrics Add influxdb.go with timestamped line protocol export for sparse one-shot events. Restore victoria.go to use proper Prometheus histograms. Update Grafana dashboards, add InfluxDB datasource, and update docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * [client] Fix metrics issues and update dev docker setup - Fix StopPush not clearing push state, preventing restart - Fix race condition reading currentConnPriority without lock in recordConnectionMetrics - Fix stale comment referencing old metrics server URL - Update docker-compose for InfluxDB: add scoped tokens, .env config, init scripts - Rename docker-compose.victoria.yml to docker-compose.yml * [client] Add anonymised peer tracking to pushed metrics Introduce peer_id and connection_pair_id tags to InfluxDB metrics. Public keys are hashed (truncated SHA-256) for anonymisation. The connection pair ID is deterministic regardless of which side computes it, enabling deduplication of reconnections in the ICE vs Relay dashboard. Also pin Grafana to v11.6.0 for file-based provisioning and fix datasource UID references. * Remove unused dependencies from go.mod and go.sum * Refactor InfluxDB ingest pipeline: extract validation logic - Move line validation logic to `validateLine` and `validateField` helper functions. - Improve error handling with structured validation and clearer separation of concerns. - Add stderr redirection for error messages in `create-tokens.sh`. * Set non-root user in Dockerfile for Ingest service * Fix Windows CI: command line too long * Remove Victoria metrics * Add hashed peer ID as Authorization header in metrics push * Revert influxdb in docker compose * Enable gzip compression and authorization validation for metrics push and ingest * Reducate code of complexity * Update debug documentation to include metrics.txt description * Increase `maxBodySize` limit to 50 MB and update gzip reader wrapping logic * Refactor deployment type detection to use URL parsing for improved accuracy * Update readme * Throttle remote config retries on fetch failure * Preserve first WG handshake timestamp, ignore rekeys * Skip adding empty metrics.txt to debug bundle in debug mode * Update default metrics server URL to https://ingest.netbird.io * Atomic metrics export-and-reset to prevent sample loss between Export and Reset calls * Fix doc * Refactor Push configuration to improve clarity and enforce minimum push interval * Remove `minPushInterval` and update push interval validation logic * Revert ExportAndReset, it is acceptable data loss * Fix metrics review issues: rename env var, remove stale infra, add tests - Rename NB_METRICS_ENABLED to NB_METRICS_PUSH_ENABLED to clarify that collection is always active (for debug bundles) and only push is opt-in - Change default config URL from staging to production (ingest.netbird.io) - Delete broken Prometheus dashboard (used non-existent metric names) - Delete unused VictoriaMetrics datasource config - Replace committed .env with .env.example containing placeholder values - Wire Grafana admin credentials through env vars in docker-compose - Make metricsStages a pointer to prevent reset-vs-write race on reconnect - Fix typed-nil interface in debug bundle path (GetClientMetrics) - Use deterministic field order in InfluxDB Export (sorted keys) - Replace Authorization header with X-Peer-ID for metrics push - Fix ingest server timeout to use time.Second instead of float - Fix gzip double-close, stale comments, trim log levels - Add tests for influxdb.go and MetricsStages * Add login duration metric, ingest tag validation, and duration bounds - Add netbird_login measurement recording login/auth duration to management server, with success/failure result tag - Validate InfluxDB tags against per-measurement allowlists in ingest server to prevent arbitrary tag injection - Cap all duration fields (*_seconds) at 300s instead of only total_seconds - Add ingest server tests for tag/field validation, bounds, and auth * Add arch tag to all metrics * Fix Grafana dashboard: add arch to drop columns, add login panels * Validate NB_METRICS_SERVER_URL is an absolute HTTP(S) URL * Address review comments: fix README wording, update stale comments * Clarify env var precedence does not bypass remote config eligibility * Remove accidentally committed pprof files --------- Co-authored-by: Viktor Liu <viktor@netbird.io>
1 parent 8276228 commit 91f0d5c

40 files changed

Lines changed: 3405 additions & 42 deletions

.github/workflows/golang-test-windows.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,15 @@ jobs:
6363
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
6464
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
6565
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
66-
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' })" >> $env:GITHUB_ENV
66+
- name: Generate test script
67+
run: |
68+
$packages = go list ./... | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' } | Where-Object { $_ -notmatch '/proxy' } | Where-Object { $_ -notmatch '/combined' }
69+
$goExe = "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe"
70+
$cmd = "$goExe test -tags=devcert -timeout 10m -p 1 $($packages -join ' ') > test-out.txt 2>&1"
71+
Set-Content -Path "${{ github.workspace }}\run-tests.cmd" -Value $cmd
6772
6873
- name: test
69-
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
74+
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "${{ github.workspace }}\run-tests.cmd"
7075
- name: test output
7176
if: ${{ always() }}
7277
run: Get-Content test-out.txt

client/internal/connect.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/netbirdio/netbird/client/iface/netstack"
2424
"github.com/netbirdio/netbird/client/internal/dns"
2525
"github.com/netbirdio/netbird/client/internal/listener"
26+
"github.com/netbirdio/netbird/client/internal/metrics"
2627
"github.com/netbirdio/netbird/client/internal/peer"
2728
"github.com/netbirdio/netbird/client/internal/profilemanager"
2829
"github.com/netbirdio/netbird/client/internal/statemanager"
@@ -50,6 +51,7 @@ type ConnectClient struct {
5051

5152
engine *Engine
5253
engineMutex sync.Mutex
54+
clientMetrics *metrics.ClientMetrics
5355
updateManager *updater.Manager
5456

5557
persistSyncResponse bool
@@ -133,10 +135,34 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
133135
}
134136
}()
135137

138+
// Stop metrics push on exit
139+
defer func() {
140+
if c.clientMetrics != nil {
141+
c.clientMetrics.StopPush()
142+
}
143+
}()
144+
136145
log.Infof("starting NetBird client version %s on %s/%s", version.NetbirdVersion(), runtime.GOOS, runtime.GOARCH)
137146

138147
nbnet.Init()
139148

149+
// Initialize metrics once at startup (always active for debug bundles)
150+
if c.clientMetrics == nil {
151+
agentInfo := metrics.AgentInfo{
152+
DeploymentType: metrics.DeploymentTypeUnknown,
153+
Version: version.NetbirdVersion(),
154+
OS: runtime.GOOS,
155+
Arch: runtime.GOARCH,
156+
}
157+
c.clientMetrics = metrics.NewClientMetrics(agentInfo)
158+
log.Debugf("initialized client metrics")
159+
160+
// Start metrics push if enabled (uses daemon context, persists across engine restarts)
161+
if metrics.IsMetricsPushEnabled() {
162+
c.clientMetrics.StartPush(c.ctx, metrics.PushConfigFromEnv())
163+
}
164+
}
165+
140166
backOff := &backoff.ExponentialBackOff{
141167
InitialInterval: time.Second,
142168
RandomizationFactor: 1,
@@ -223,6 +249,16 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
223249
mgmNotifier := statusRecorderToMgmConnStateNotifier(c.statusRecorder)
224250
mgmClient.SetConnStateListener(mgmNotifier)
225251

252+
// Update metrics with actual deployment type after connection
253+
deploymentType := metrics.DetermineDeploymentType(mgmClient.GetServerURL())
254+
agentInfo := metrics.AgentInfo{
255+
DeploymentType: deploymentType,
256+
Version: version.NetbirdVersion(),
257+
OS: runtime.GOOS,
258+
Arch: runtime.GOARCH,
259+
}
260+
c.clientMetrics.UpdateAgentInfo(agentInfo, myPrivateKey.PublicKey().String())
261+
226262
log.Debugf("connected to the Management service %s", c.config.ManagementURL.Host)
227263
defer func() {
228264
if err = mgmClient.Close(); err != nil {
@@ -231,8 +267,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
231267
}()
232268

233269
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Netbird config
270+
loginStarted := time.Now()
234271
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey, c.config)
235272
if err != nil {
273+
c.clientMetrics.RecordLoginDuration(engineCtx, time.Since(loginStarted), false)
236274
log.Debug(err)
237275
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
238276
state.Set(StatusNeedsLogin)
@@ -241,6 +279,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
241279
}
242280
return wrapErr(err)
243281
}
282+
c.clientMetrics.RecordLoginDuration(engineCtx, time.Since(loginStarted), true)
244283
c.statusRecorder.MarkManagementConnected()
245284

246285
localPeerState := peer.LocalPeerState{
@@ -317,6 +356,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
317356
Checks: checks,
318357
StateManager: stateManager,
319358
UpdateManager: c.updateManager,
359+
ClientMetrics: c.clientMetrics,
320360
}, mobileDependency)
321361
engine.SetSyncResponsePersistence(c.persistSyncResponse)
322362
c.engine = engine

client/internal/debug/debug.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ resolved_domains.txt: Anonymized resolved domain IP addresses from the status re
5252
config.txt: Anonymized configuration information of the NetBird client.
5353
network_map.json: Anonymized sync response containing peer configurations, routes, DNS settings, and firewall rules.
5454
state.json: Anonymized client state dump containing netbird states for the active profile.
55+
metrics.txt: Buffered client metrics in InfluxDB line protocol format. Only present when metrics collection is enabled. Peer identifiers are anonymized.
5556
mutex.prof: Mutex profiling information.
5657
goroutine.prof: Goroutine profiling information.
5758
block.prof: Block profiling information.
@@ -218,6 +219,11 @@ const (
218219
darwinStdoutLogPath = "/var/log/netbird.err.log"
219220
)
220221

222+
// MetricsExporter is an interface for exporting metrics
223+
type MetricsExporter interface {
224+
Export(w io.Writer) error
225+
}
226+
221227
type BundleGenerator struct {
222228
anonymizer *anonymize.Anonymizer
223229

@@ -228,6 +234,7 @@ type BundleGenerator struct {
228234
logPath string
229235
cpuProfile []byte
230236
refreshStatus func() // Optional callback to refresh status before bundle generation
237+
clientMetrics MetricsExporter
231238

232239
anonymize bool
233240
includeSystemInfo bool
@@ -249,6 +256,7 @@ type GeneratorDependencies struct {
249256
LogPath string
250257
CPUProfile []byte
251258
RefreshStatus func() // Optional callback to refresh status before bundle generation
259+
ClientMetrics MetricsExporter
252260
}
253261

254262
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
@@ -267,6 +275,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
267275
logPath: deps.LogPath,
268276
cpuProfile: deps.CPUProfile,
269277
refreshStatus: deps.RefreshStatus,
278+
clientMetrics: deps.ClientMetrics,
270279

271280
anonymize: cfg.Anonymize,
272281
includeSystemInfo: cfg.IncludeSystemInfo,
@@ -350,6 +359,10 @@ func (g *BundleGenerator) createArchive() error {
350359
log.Errorf("failed to add corrupted state files to debug bundle: %v", err)
351360
}
352361

362+
if err := g.addMetrics(); err != nil {
363+
log.Errorf("failed to add metrics to debug bundle: %v", err)
364+
}
365+
353366
if err := g.addWgShow(); err != nil {
354367
log.Errorf("failed to add wg show output: %v", err)
355368
}
@@ -746,6 +759,30 @@ func (g *BundleGenerator) addCorruptedStateFiles() error {
746759
return nil
747760
}
748761

762+
func (g *BundleGenerator) addMetrics() error {
763+
if g.clientMetrics == nil {
764+
log.Debugf("skipping metrics in debug bundle: no metrics collector")
765+
return nil
766+
}
767+
768+
var buf bytes.Buffer
769+
if err := g.clientMetrics.Export(&buf); err != nil {
770+
return fmt.Errorf("export metrics: %w", err)
771+
}
772+
773+
if buf.Len() == 0 {
774+
log.Debugf("skipping metrics.txt in debug bundle: no metrics data")
775+
return nil
776+
}
777+
778+
if err := g.addFileToZip(&buf, "metrics.txt"); err != nil {
779+
return fmt.Errorf("add metrics file to zip: %w", err)
780+
}
781+
782+
log.Debugf("added metrics to debug bundle")
783+
return nil
784+
}
785+
749786
func (g *BundleGenerator) addLogfile() error {
750787
if g.logPath == "" {
751788
log.Debugf("skipping empty log file in debug bundle")

client/internal/engine.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"github.com/netbirdio/netbird/client/internal/dnsfwd"
3939
"github.com/netbirdio/netbird/client/internal/expose"
4040
"github.com/netbirdio/netbird/client/internal/ingressgw"
41+
"github.com/netbirdio/netbird/client/internal/metrics"
4142
"github.com/netbirdio/netbird/client/internal/netflow"
4243
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
4344
"github.com/netbirdio/netbird/client/internal/networkmonitor"
@@ -149,6 +150,7 @@ type EngineServices struct {
149150
Checks []*mgmProto.Checks
150151
StateManager *statemanager.Manager
151152
UpdateManager *updater.Manager
153+
ClientMetrics *metrics.ClientMetrics
152154
}
153155

154156
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
@@ -229,6 +231,9 @@ type Engine struct {
229231

230232
probeStunTurn *relay.StunTurnProbe
231233

234+
// clientMetrics collects and pushes metrics
235+
clientMetrics *metrics.ClientMetrics
236+
232237
jobExecutor *jobexec.Executor
233238
jobExecutorWG sync.WaitGroup
234239

@@ -272,6 +277,7 @@ func NewEngine(
272277
checks: services.Checks,
273278
probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL),
274279
jobExecutor: jobexec.NewExecutor(),
280+
clientMetrics: services.ClientMetrics,
275281
updateManager: services.UpdateManager,
276282
}
277283

@@ -813,7 +819,9 @@ func (e *Engine) handleAutoUpdateVersion(autoUpdateSettings *mgmProto.AutoUpdate
813819
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
814820
started := time.Now()
815821
defer func() {
816-
log.Infof("sync finished in %s", time.Since(started))
822+
duration := time.Since(started)
823+
log.Infof("sync finished in %s", duration)
824+
e.clientMetrics.RecordSyncDuration(e.ctx, duration)
817825
}()
818826
e.syncMsgMux.Lock()
819827
defer e.syncMsgMux.Unlock()
@@ -1061,6 +1069,7 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
10611069
StatusRecorder: e.statusRecorder,
10621070
SyncResponse: syncResponse,
10631071
LogPath: e.config.LogPath,
1072+
ClientMetrics: e.clientMetrics,
10641073
RefreshStatus: func() {
10651074
e.RunHealthProbes(true)
10661075
},
@@ -1515,11 +1524,12 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
15151524
}
15161525

15171526
serviceDependencies := peer.ServiceDependencies{
1518-
StatusRecorder: e.statusRecorder,
1519-
Signaler: e.signaler,
1520-
IFaceDiscover: e.mobileDep.IFaceDiscover,
1521-
RelayManager: e.relayManager,
1522-
SrWatcher: e.srWatcher,
1527+
StatusRecorder: e.statusRecorder,
1528+
Signaler: e.signaler,
1529+
IFaceDiscover: e.mobileDep.IFaceDiscover,
1530+
RelayManager: e.relayManager,
1531+
SrWatcher: e.srWatcher,
1532+
MetricsRecorder: e.clientMetrics,
15231533
}
15241534
peerConn, err := peer.NewConn(config, serviceDependencies)
15251535
if err != nil {
@@ -1816,6 +1826,11 @@ func (e *Engine) GetExposeManager() *expose.Manager {
18161826
return e.exposeManager
18171827
}
18181828

1829+
// GetClientMetrics returns the client metrics
1830+
func (e *Engine) GetClientMetrics() *metrics.ClientMetrics {
1831+
return e.clientMetrics
1832+
}
1833+
18191834
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
18201835
iface, err := net.InterfaceByName(ifaceName)
18211836
if err != nil {

client/internal/engine_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
828828
WgPrivateKey: key,
829829
WgPort: 33100,
830830
MTU: iface.DefaultMTU,
831-
}, EngineServices{
831+
}, EngineServices{
832832
SignalClient: &signal.MockClient{},
833833
MgmClient: &mgmt.MockClient{},
834834
RelayManager: relayMgr,
@@ -1035,7 +1035,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
10351035
WgPrivateKey: key,
10361036
WgPort: 33100,
10371037
MTU: iface.DefaultMTU,
1038-
}, EngineServices{
1038+
}, EngineServices{
10391039
SignalClient: &signal.MockClient{},
10401040
MgmClient: &mgmt.MockClient{},
10411041
RelayManager: relayMgr,
@@ -1566,7 +1566,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
15661566
}
15671567

15681568
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
1569-
e, err := NewEngine(ctx, cancel, conf, EngineServices{
1569+
e, err := NewEngine(ctx, cancel, conf, EngineServices{
15701570
SignalClient: signalClient,
15711571
MgmClient: mgmtClient,
15721572
RelayManager: relayMgr,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package metrics
2+
3+
// ConnectionType represents the type of peer connection
4+
type ConnectionType string
5+
6+
const (
7+
// ConnectionTypeICE represents a direct peer-to-peer connection using ICE
8+
ConnectionTypeICE ConnectionType = "ice"
9+
10+
// ConnectionTypeRelay represents a relayed connection
11+
ConnectionTypeRelay ConnectionType = "relay"
12+
)
13+
14+
// String returns the string representation of the connection type
15+
func (c ConnectionType) String() string {
16+
return string(c)
17+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package metrics
2+
3+
import (
4+
"net/url"
5+
"strings"
6+
)
7+
8+
// DeploymentType represents the type of NetBird deployment
9+
type DeploymentType int
10+
11+
const (
12+
// DeploymentTypeUnknown represents an unknown or uninitialized deployment type
13+
DeploymentTypeUnknown DeploymentType = iota
14+
15+
// DeploymentTypeCloud represents a cloud-hosted NetBird deployment
16+
DeploymentTypeCloud
17+
18+
// DeploymentTypeSelfHosted represents a self-hosted NetBird deployment
19+
DeploymentTypeSelfHosted
20+
)
21+
22+
// String returns the string representation of the deployment type
23+
func (d DeploymentType) String() string {
24+
switch d {
25+
case DeploymentTypeCloud:
26+
return "cloud"
27+
case DeploymentTypeSelfHosted:
28+
return "selfhosted"
29+
default:
30+
return "unknown"
31+
}
32+
}
33+
34+
// DetermineDeploymentType determines if the deployment is cloud or self-hosted
35+
// based on the management URL string
36+
func DetermineDeploymentType(managementURL string) DeploymentType {
37+
if managementURL == "" {
38+
return DeploymentTypeUnknown
39+
}
40+
41+
u, err := url.Parse(managementURL)
42+
if err != nil {
43+
return DeploymentTypeSelfHosted
44+
}
45+
46+
if strings.ToLower(u.Hostname()) == "api.netbird.io" {
47+
return DeploymentTypeCloud
48+
}
49+
50+
return DeploymentTypeSelfHosted
51+
}

0 commit comments

Comments
 (0)