From db0f5fa4f55773a88b6d0d251f201666a10f4032 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Sat, 27 Jun 2026 22:57:55 +0200 Subject: [PATCH] [management] Add agent-network telemetry metrics (#6561) Surface agent-network adoption and usage in the self-hosted metrics worker: distinct accounts, providers, policies, budget rules, accounts with log collection enabled, and aggregated input/output tokens plus cost. Tokens and cost are summed from agent_network_request_usage (the always-written per-request ledger) so the figures are accurate regardless of the log-collection toggle and carry no double-counting. All values come from a handful of indexed aggregate queries run only on the worker's periodic tick. Adds store.AgentNetworkMetrics with GetAgentNetworkMetrics on the Store interface, the SqlStore implementation, and a zero-valued FileStore stub. --- management/server/metrics/selfhosted.go | 16 ++++++++ management/server/metrics/selfhosted_test.go | 15 +++++++ management/server/store/file_store.go | 6 +++ .../server/store/sql_store_agentnetwork.go | 41 +++++++++++++++++++ management/server/store/store.go | 31 ++++++++++++++ .../server/store/store_mock_agentnetwork.go | 15 +++++++ 6 files changed, 124 insertions(+) diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index efe50c88f..43fbec15d 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -55,6 +55,7 @@ type DataSource interface { GetStoreEngine() types.Engine GetCustomDomainsCounts(ctx context.Context) (total int64, validated int64, err error) GetProxyMetrics(ctx context.Context) (store.ProxyMetrics, error) + GetAgentNetworkMetrics(ctx context.Context) (store.AgentNetworkMetrics, error) } // ConnManager peer connection manager that holds state for current active connections @@ -413,6 +414,13 @@ func (w *Worker) generateProperties(ctx context.Context) properties { log.WithContext(ctx).Debugf("collect proxy metrics: %v", err) } + // Agent-network adoption + usage, aggregated across all accounts in a few + // cheap queries; nil on FileStore. + agentNetworkMetrics, err := w.dataSource.GetAgentNetworkMetrics(ctx) + if err != nil { + log.WithContext(ctx).Debugf("collect agent network metrics: %v", err) + } + minActivePeerVersion, maxActivePeerVersion := getMinMaxVersion(peerActiveVersions) metricsProperties["uptime"] = uptime metricsProperties["accounts"] = accounts @@ -471,6 +479,14 @@ func (w *Worker) generateProperties(ctx context.Context) properties { metricsProperties["proxies_connected"] = proxyMetrics.ProxiesConnected metricsProperties["custom_domains"] = customDomains metricsProperties["custom_domains_validated"] = customDomainsValidated + metricsProperties["agent_network_accounts"] = agentNetworkMetrics.Accounts + metricsProperties["agent_network_providers"] = agentNetworkMetrics.Providers + metricsProperties["agent_network_policies"] = agentNetworkMetrics.Policies + metricsProperties["agent_network_budget_rules"] = agentNetworkMetrics.BudgetRules + metricsProperties["agent_network_log_collection_enabled"] = agentNetworkMetrics.LogCollectionEnabled + metricsProperties["agent_network_input_tokens"] = agentNetworkMetrics.InputTokens + metricsProperties["agent_network_output_tokens"] = agentNetworkMetrics.OutputTokens + metricsProperties["agent_network_cost_usd"] = agentNetworkMetrics.CostUSD for targetType, count := range servicesTargetType { metricsProperties["services_target_type_"+string(targetType)] = count diff --git a/management/server/metrics/selfhosted_test.go b/management/server/metrics/selfhosted_test.go index ca9e10262..1fef89a2c 100644 --- a/management/server/metrics/selfhosted_test.go +++ b/management/server/metrics/selfhosted_test.go @@ -277,6 +277,21 @@ func (mockDatasource) GetProxyMetrics(_ context.Context) (store.ProxyMetrics, er }, nil } +// GetAgentNetworkMetrics returns canned agent-network counts so the +// generateProperties test can assert the adoption/usage signals end-to-end. +func (mockDatasource) GetAgentNetworkMetrics(_ context.Context) (store.AgentNetworkMetrics, error) { + return store.AgentNetworkMetrics{ + Accounts: 2, + Providers: 5, + Policies: 3, + BudgetRules: 1, + LogCollectionEnabled: 2, + InputTokens: 1000, + OutputTokens: 500, + CostUSD: 1.25, + }, nil +} + // TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties func TestGenerateProperties(t *testing.T) { ds := mockDatasource{} diff --git a/management/server/store/file_store.go b/management/server/store/file_store.go index bcf563cd0..a776a8b42 100644 --- a/management/server/store/file_store.go +++ b/management/server/store/file_store.go @@ -280,3 +280,9 @@ func (s *FileStore) GetCustomDomainsCounts(_ context.Context) (int64, int64, err func (s *FileStore) GetProxyMetrics(_ context.Context) (ProxyMetrics, error) { return ProxyMetrics{}, nil } + +// GetAgentNetworkMetrics is a no-op for FileStore — agent-network state isn't +// persisted in the JSON file format. +func (s *FileStore) GetAgentNetworkMetrics(_ context.Context) (AgentNetworkMetrics, error) { + return AgentNetworkMetrics{}, nil +} diff --git a/management/server/store/sql_store_agentnetwork.go b/management/server/store/sql_store_agentnetwork.go index bb265437b..b0df0cd2a 100644 --- a/management/server/store/sql_store_agentnetwork.go +++ b/management/server/store/sql_store_agentnetwork.go @@ -3,6 +3,7 @@ package store import ( "context" "errors" + "fmt" "math" "time" @@ -38,6 +39,46 @@ func (s *SqlStore) GetAllAgentNetworkProviders(ctx context.Context, lockStrength return providers, nil } +// GetAgentNetworkMetrics returns aggregated agent-network adoption + usage +// counts for the self-hosted metrics worker. Each value is a single cheap +// aggregate; token/cost are summed over the always-collected per-request usage +// ledger (independent of the log-collection toggle) so they reflect real usage. +func (s *SqlStore) GetAgentNetworkMetrics(ctx context.Context) (AgentNetworkMetrics, error) { + var m AgentNetworkMetrics + db := s.db.WithContext(ctx) + + // Providers + distinct adopting accounts in one round-trip. + provRow := db.Model(&agentNetworkTypes.Provider{}). + Select("COUNT(*) AS providers, COUNT(DISTINCT account_id) AS accounts").Row() + if err := provRow.Scan(&m.Providers, &m.Accounts); err != nil { + return AgentNetworkMetrics{}, fmt.Errorf("scan agent network provider metrics: %w", err) + } + + if err := db.Model(&agentNetworkTypes.Policy{}).Count(&m.Policies).Error; err != nil { + return AgentNetworkMetrics{}, fmt.Errorf("count agent network policies: %w", err) + } + + if err := db.Model(&agentNetworkTypes.AccountBudgetRule{}).Count(&m.BudgetRules).Error; err != nil { + return AgentNetworkMetrics{}, fmt.Errorf("count agent network budget rules: %w", err) + } + + if err := db.Model(&agentNetworkTypes.Settings{}). + Where("enable_log_collection = ?", true).Count(&m.LogCollectionEnabled).Error; err != nil { + return AgentNetworkMetrics{}, fmt.Errorf("count agent network log-collection accounts: %w", err) + } + + // COALESCE so an empty ledger scans as 0 instead of NULL. + usageRow := db.Model(&agentNetworkTypes.AgentNetworkUsage{}). + Select("COALESCE(SUM(input_tokens), 0) AS input_tokens, " + + "COALESCE(SUM(output_tokens), 0) AS output_tokens, " + + "COALESCE(SUM(cost_usd), 0) AS cost_usd").Row() + if err := usageRow.Scan(&m.InputTokens, &m.OutputTokens, &m.CostUSD); err != nil { + return AgentNetworkMetrics{}, fmt.Errorf("scan agent network usage metrics: %w", err) + } + + return m, nil +} + func (s *SqlStore) GetAccountAgentNetworkProviders(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Provider, error) { tx := s.db if lockStrength != LockingStrengthNone { diff --git a/management/server/store/store.go b/management/server/store/store.go index 3ba6facc3..f678ccc89 100644 --- a/management/server/store/store.go +++ b/management/server/store/store.go @@ -334,6 +334,11 @@ type Store interface { // return a zero-valued struct. GetProxyMetrics(ctx context.Context) (ProxyMetrics, error) + // GetAgentNetworkMetrics returns aggregated agent-network adoption + usage + // counts for the self-hosted metrics worker. Self-hosted only — file-based + // stores return a zero-valued struct. + GetAgentNetworkMetrics(ctx context.Context) (AgentNetworkMetrics, error) + GetRoutingPeerNetworks(ctx context.Context, accountID, peerID string) ([]string, error) // Agent Network persistence (providers, policies, guardrails, settings). @@ -389,6 +394,32 @@ type ProxyMetrics struct { ProxiesConnected int64 } +// AgentNetworkMetrics aggregates self-hosted agent-network adoption + usage +// signals surfaced to the telemetry payload. Each field is best-effort: when a +// store cannot answer (e.g. FileStore) all fields are zero. +type AgentNetworkMetrics struct { + // Accounts is the number of distinct accounts with at least one provider + // configured (agent-network adoption). + Accounts int64 + // Providers is the total number of configured providers across all accounts. + Providers int64 + // Policies is the total number of agent-network policies across all accounts. + Policies int64 + // BudgetRules is the total number of account-level budget rules ("budget + // limits") across all accounts. + BudgetRules int64 + // LogCollectionEnabled is the number of accounts that have agent-network + // log collection turned on. + LogCollectionEnabled int64 + // InputTokens / OutputTokens / CostUSD are summed over the always-collected + // per-request usage ledger (agent_network_request_usage), independent of the + // log-collection toggle. They reflect total metered LLM usage served through + // agent networks. + InputTokens int64 + OutputTokens int64 + CostUSD float64 +} + const ( postgresDsnEnv = "NB_STORE_ENGINE_POSTGRES_DSN" postgresDsnEnvLegacy = "NETBIRD_STORE_ENGINE_POSTGRES_DSN" diff --git a/management/server/store/store_mock_agentnetwork.go b/management/server/store/store_mock_agentnetwork.go index 8ad1498c1..1ca866e63 100644 --- a/management/server/store/store_mock_agentnetwork.go +++ b/management/server/store/store_mock_agentnetwork.go @@ -25,6 +25,21 @@ func (mr *MockStoreMockRecorder) GetAllAgentNetworkProviders(ctx, lockStrength i return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllAgentNetworkProviders", reflect.TypeOf((*MockStore)(nil).GetAllAgentNetworkProviders), ctx, lockStrength) } +// GetAgentNetworkMetrics mocks base method. +func (m *MockStore) GetAgentNetworkMetrics(ctx context.Context) (AgentNetworkMetrics, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAgentNetworkMetrics", ctx) + ret0, _ := ret[0].(AgentNetworkMetrics) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAgentNetworkMetrics indicates an expected call of GetAgentNetworkMetrics. +func (mr *MockStoreMockRecorder) GetAgentNetworkMetrics(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgentNetworkMetrics", reflect.TypeOf((*MockStore)(nil).GetAgentNetworkMetrics), ctx) +} + // GetAccountAgentNetworkProviders mocks base method. func (m *MockStore) GetAccountAgentNetworkProviders(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*agentNetworkTypes.Provider, error) { m.ctrl.T.Helper()