Skip to content

[client] Add local Prometheus metrics endpoint - #6689

Merged
lixmal merged 9 commits into
mainfrom
client-local-metrics
Aug 27, 2026
Merged

[client] Add local Prometheus metrics endpoint#6689
lixmal merged 9 commits into
mainfrom
client-local-metrics

Conversation

@lixmal

@lixmal lixmal commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Adds an opt-in Prometheus /metrics endpoint to the client daemon so deployments can scrape connection health locally (requested for a RedHat evaluation: latency, connected peers, P2P vs relay).

  • Add netbird up --enable-local-metrics and --local-metrics-address (default 127.0.0.1:9191); the setting persists in the profile config and travels through SetConfig/Login like other config flags
  • Expose the existing client metrics (peer connection stage durations, sync and login durations) by mirroring them into a Prometheus registry behind the same recording interface
  • Add scrape-time gauges for current state: management/signal connectivity, known peers, connected peers by connection type, and per-peer latency for direct connections
  • Add a Grafana dashboard for the client next to the existing management/signal/relay dashboards
  • Gate the new settings behind --disable-update-settings and MDM policy (enableLocalMetrics, localMetricsAddress keys)
  • Regenerate the daemon JSON gateway, which was missing routes for recently added RPCs

All labels are bounded enums except the per-peer latency series, which carries the peer FQDN only.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)
  • This change does not modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — OR I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See CONTRIBUTING.md.

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

netbirdio/docs#952

Summary by CodeRabbit

  • New Features
    • Added an optional local Prometheus /metrics endpoint for client connectivity, peer states, latency, and connection performance.
    • Added CLI and configuration options to enable local metrics and set its listening address.
    • Added MDM policy support for managing local metrics settings.
    • Added a Grafana dashboard for visualizing client metrics.
  • Documentation
    • Debug bundles now include local metrics configuration details.
  • Bug Fixes
    • Improved connection-duration metric calculations and reporting consistency.
    • Added safeguards and privilege checks when exposing metrics beyond the local device.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1aeac690-5922-41e2-bbba-843d30f0e0af

📥 Commits

Reviewing files that changed from the base of the PR and between 352bbee and 8b97432.

📒 Files selected for processing (1)
  • client/cmd/up.go

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

Adds a local Prometheus /metrics endpoint, Prometheus client metric collection, CLI and daemon configuration, MDM policy handling, server lifecycle integration, access enforcement, tests, dependency updates, and a Grafana dashboard.

Changes

Local metrics feature

Layer / File(s) Summary
Prometheus client metrics wrapper
client/internal/metrics/*
Adds Prometheus histograms for connection, synchronization, phase, and login durations while continuing to delegate existing metric recording.
Local metrics manager and collector
client/internal/localmetrics/*, client/internal/peer/status.go
Adds the /metrics server, peer-state snapshots, connectivity and latency gauges, optional engine metrics, lifecycle reconciliation, loopback validation, and recovery tests.
Configuration and daemon contracts
client/internal/profilemanager/config.go, client/mdm/*, client/proto/daemon.proto, client/internal/debug/debug.go
Adds local metrics configuration, daemon request fields, MDM keys and canonicalization, debug output, and configuration tests.
CLI, server lifecycle, and access enforcement
client/cmd/*, client/server/*
Adds CLI flags, request propagation, server reconciliation, MDM conflict checks, privilege checks, and persistence tests.
Dependency and Grafana observability support
go.mod, infrastructure_files/observability/grafana/dashboards/client.json
Updates Prometheus module requirements and adds dashboard panels for client connectivity and timing metrics.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: ⚪ Minimal · up to 8b974

The PR adds an opt-in local metrics endpoint and related configuration without any identified merge-blocking risk; it is merge-ready after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Server
  participant LocalMetrics
  participant ClientEngine
  participant Prometheus
  CLI->>Server: Configure local metrics
  Server->>LocalMetrics: Reconcile enabled state and address
  LocalMetrics->>ClientEngine: Request Prometheus gatherer
  Prometheus->>LocalMetrics: GET /metrics
  LocalMetrics->>ClientEngine: Gather engine metrics
  LocalMetrics-->>Prometheus: Return local and engine metrics
Loading

Suggested reviewers: theodorsm

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the feature, implementation scope, tests, and documentation. However, the required issue ticket or approved discussion link is missing, and the checklist contains a contradict… Add the required issue ticket or approved NetBird team discussion link. Correct the checklist statement to acknowledge the new feature and CLI flags, and confirm local testing if performed.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 54 functions across 22 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding a local Prometheus metrics endpoint to the client.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the feature, implementation scope, tests, and documentation. However, the required issue ticket or approved discussion link is missing, and the checklist contains a contradictory claim that the change does not introduce a new feature or modify CLI flags.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch client-local-metrics

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
client/internal/localmetrics/localmetrics.go (1)

60-63: 🔒 Security & Privacy | 🔵 Trivial

Consider warning when binding to a non-loopback address.

The endpoint exposes peer FQDNs, latency, and connectivity state without authentication. Default is loopback-only, but addr is fully user-controlled; a log warning (or explicit opt-in) when the host isn't loopback would help users avoid accidental exposure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/internal/localmetrics/localmetrics.go` around lines 60 - 63, The local
metrics endpoint in Manager.Reconcile can bind to any user-supplied addr, which
may accidentally expose sensitive peer and connectivity data. Update Reconcile
to detect when addr is not loopback after defaulting to DefaultListenAddress,
and emit a clear warning (or require explicit opt-in) before starting the
listener. Keep the check near the address selection logic so the behavior is
obvious and easy to maintain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/internal/localmetrics/localmetrics.go`:
- Line 94: The local metrics server created in `Start` uses `http.Server`
without any timeout protections, leaving it vulnerable to slow client
connections. Update the `srv := &http.Server{...}` construction to set
appropriate `ReadTimeout`, `ReadHeaderTimeout`, `WriteTimeout`, and
`IdleTimeout` values, keeping the existing `Addr` and `Handler` behavior intact.
Use the `Start` function and the `http.Server` setup as the place to apply these
limits.

---

Nitpick comments:
In `@client/internal/localmetrics/localmetrics.go`:
- Around line 60-63: The local metrics endpoint in Manager.Reconcile can bind to
any user-supplied addr, which may accidentally expose sensitive peer and
connectivity data. Update Reconcile to detect when addr is not loopback after
defaulting to DefaultListenAddress, and emit a clear warning (or require
explicit opt-in) before starting the listener. Keep the check near the address
selection logic so the behavior is obvious and easy to maintain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7f956574-47e5-405e-a5e6-4581c2baf2ed

📥 Commits

Reviewing files that changed from the base of the PR and between d0d6dd4 and c4cfd8c.

⛔ Files ignored due to path filters (1)
  • client/proto/daemon.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (15)
  • client/cmd/root.go
  • client/cmd/up.go
  • client/internal/localmetrics/localmetrics.go
  • client/internal/localmetrics/localmetrics_test.go
  • client/internal/metrics/influxdb.go
  • client/internal/metrics/metrics.go
  • client/internal/metrics/metrics_default.go
  • client/internal/metrics/prometheus.go
  • client/internal/peer/status.go
  • client/internal/profilemanager/config.go
  • client/proto/daemon.proto
  • client/server/server.go
  • client/server/setconfig_test.go
  • go.mod
  • infrastructure_files/observability/grafana/dashboards/client.json

Comment thread client/internal/localmetrics/localmetrics.go Outdated
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Release artifacts

Built for PR head 8b97432 in workflow run #18247.

Artifact Link
All release artifacts Download
Linux packages Download
Windows packages Download
macOS packages Download
UI artifacts Download
UI GTK3 artifacts Download
UI macOS artifacts Download

GHCR images (amd64)

This comment is updated by the Release workflow. Artifact links expire according to the workflow retention policy.

@lixmal
lixmal force-pushed the client-local-metrics branch from cd0ecae to 6d9cb90 Compare July 7, 2026 17:23
@lixmal
lixmal force-pushed the client-local-metrics branch from 35e157d to e93fd6c Compare July 15, 2026 19:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/server/mdm.go`:
- Around line 304-305: Preserve nil-versus-explicit-empty presence when checking
LocalMetricsAddress by replacing the conflictString calls using
msg.GetLocalMetricsAddress() with a pointer-aware comparison. Apply this change
at client/server/mdm.go lines 304-305 and 431-432, while retaining the existing
managed-field conflict behavior for both absent and supplied values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e464b486-7137-49d6-a214-0c83007fc044

📥 Commits

Reviewing files that changed from the base of the PR and between 35e157d and e93fd6c.

⛔ Files ignored due to path filters (1)
  • client/proto/daemon.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (20)
  • client/cmd/root.go
  • client/cmd/up.go
  • client/internal/debug/debug.go
  • client/internal/localmetrics/localmetrics.go
  • client/internal/localmetrics/localmetrics_test.go
  • client/internal/metrics/influxdb.go
  • client/internal/metrics/metrics.go
  • client/internal/metrics/metrics_default.go
  • client/internal/metrics/prometheus.go
  • client/internal/peer/status.go
  • client/internal/profilemanager/config.go
  • client/internal/profilemanager/config_mdm_test.go
  • client/mdm/policy.go
  • client/proto/daemon.proto
  • client/server/mdm.go
  • client/server/server.go
  • client/server/setconfig_mdm_test.go
  • client/server/setconfig_test.go
  • go.mod
  • infrastructure_files/observability/grafana/dashboards/client.json
🚧 Files skipped from review as they are similar to previous changes (18)
  • client/internal/localmetrics/localmetrics_test.go
  • infrastructure_files/observability/grafana/dashboards/client.json
  • client/server/setconfig_mdm_test.go
  • client/server/setconfig_test.go
  • client/internal/profilemanager/config_mdm_test.go
  • client/internal/metrics/metrics.go
  • client/cmd/up.go
  • client/internal/localmetrics/localmetrics.go
  • client/internal/metrics/metrics_default.go
  • client/internal/metrics/influxdb.go
  • client/internal/profilemanager/config.go
  • client/internal/debug/debug.go
  • client/cmd/root.go
  • client/internal/metrics/prometheus.go
  • client/proto/daemon.proto
  • client/internal/peer/status.go
  • go.mod
  • client/server/server.go

Comment thread client/server/mdm.go Outdated
# Conflicts:
#	client/internal/debug/debug.go
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

1 similar comment
@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@theodorsm theodorsm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Foreground mode accepts the local metrics flags, but does not serve the endpoint. Do we want to support this feature for foreground mode?

ch <- prometheus.MustNewConstMetric(c.signalConnected, prometheus.GaugeValue, boolToFloat(c.status.GetSignalState().Connected))

peers := c.status.GetPeerStates()
ch <- prometheus.MustNewConstMetric(c.peersTotal, prometheus.GaugeValue, float64(len(peers)))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not account for offline peers, which is also known peers. We should export Status.numOfPeers and use it here (or add offline peers to GetPeerStatus) if the intention of this metric is to mirror status "peers count: x/y connected".

Currently this is what status would show:

Image

And this is what grafana would show:
Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread client/internal/peer/status.go Outdated
d.mux.RLock()
defer d.mux.RUnlock()

states := make([]State, 0, len(d.peers))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include offline peers too, like GetFullStatus ? Ref: https://github.com/netbirdio/netbird/pull/6689/changes#r3863137373

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread client/mdm/policy.go
Comment on lines +50 to +51
KeyEnableLocalMetrics = "enableLocalMetrics"
KeyLocalMetricsAddress = "localMetricsAddress"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KeyEnableLocalMetrics and KeyLocalMetricsAddress are never added to allKeys (client/mdm/canonical_loaders.go), so the Windows registry and macOS plist loaders discard them as unknown.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread client/server/server.go
return nil, fmt.Errorf("failed to get default profile config: %w", err)
}

s.config = config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing localMetrics.Reconcile. The profile we switch to might have different metrics settings.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment thread client/server/server.go
return nil, fmt.Errorf("failed to get active profile config: %w", err)
}
s.mutex.Lock()
s.config = config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing localMetrics.Reconcile. The profile might have changed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

log.Infof("serving local metrics on http://%s/metrics", addr)
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Errorf("failed to serve local metrics on %s: %v", addr, err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should reset the m.srv and m.addr fields like we do in stop().

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if addr == "" {
addr = DefaultListenAddress
}
warnIfNotLoopback(addr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should gate it behind a privilege check like we do for ssh rather than just logging.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@sonarqubecloud

Copy link
Copy Markdown

@lixmal
lixmal merged commit 63c26be into main Aug 27, 2026
69 of 71 checks passed
@lixmal
lixmal deleted the client-local-metrics branch August 27, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants