diff --git a/builtin/logical/pki/acme_billing_test.go b/builtin/logical/pki/acme_billing_test.go index 1911d6804e64..b1948d7be29c 100644 --- a/builtin/logical/pki/acme_billing_test.go +++ b/builtin/logical/pki/acme_billing_test.go @@ -131,10 +131,10 @@ func validateClientCount(t *testing.T, client *api.Client, mount string, expecte require.NotNil(t, resp) require.NotNil(t, resp.Data) - require.Contains(t, resp.Data, "non_entity_clients") + require.Contains(t, resp.Data, "acme_clients") require.Contains(t, resp.Data, "months") - rawCount := resp.Data["non_entity_clients"].(json.Number) + rawCount := resp.Data["acme_clients"].(json.Number) count, err := rawCount.Int64() require.NoError(t, err, "failed to parse number as int64: "+rawCount.String()) @@ -158,8 +158,8 @@ func validateClientCount(t *testing.T, client *api.Client, mount string, expecte // Validate this month's aggregate counts match the overall value. require.Contains(t, monthlyInfo, "counts", "expected monthly info to contain a count key") monthlyCounts := monthlyInfo["counts"].(map[string]interface{}) - require.Contains(t, monthlyCounts, "non_entity_clients", "expected month[0].counts to contain a non_entity_clients key") - monthlyCountNonEntityRaw := monthlyCounts["non_entity_clients"].(json.Number) + require.Contains(t, monthlyCounts, "acme_clients", "expected month[0].counts to contain a non_entity_clients key") + monthlyCountNonEntityRaw := monthlyCounts["acme_clients"].(json.Number) monthlyCountNonEntity, err := monthlyCountNonEntityRaw.Int64() require.NoError(t, err, "failed to parse number as int64: "+monthlyCountNonEntityRaw.String()) require.Equal(t, count, monthlyCountNonEntity, "expected equal values for non entity client counts") @@ -194,8 +194,8 @@ func validateClientCount(t *testing.T, client *api.Client, mount string, expecte // This namespace must have a non-empty aggregate non-entity count. require.Contains(t, namespace, "counts", "expected monthly.namespaces[%v] to contain a counts key", index) namespaceCounts := namespace["counts"].(map[string]interface{}) - require.Contains(t, namespaceCounts, "non_entity_clients", "expected namespace counts to contain a non_entity_clients key") - namespaceCountNonEntityRaw := namespaceCounts["non_entity_clients"].(json.Number) + require.Contains(t, namespaceCounts, "acme_clients", "expected namespace counts to contain a non_entity_clients key") + namespaceCountNonEntityRaw := namespaceCounts["acme_clients"].(json.Number) namespaceCountNonEntity, err := namespaceCountNonEntityRaw.Int64() require.NoError(t, err, "failed to parse number as int64: "+namespaceCountNonEntityRaw.String()) require.Greater(t, namespaceCountNonEntity, int64(0), "expected at least one non-entity client count value in the namespace") @@ -217,8 +217,8 @@ func validateClientCount(t *testing.T, client *api.Client, mount string, expecte // This mount must also have a non-empty non-entity client count. require.Contains(t, mountInfo, "counts", "expected monthly.namespaces[%v].mounts[%v] to contain a counts key", index, mountIndex) mountCounts := mountInfo["counts"].(map[string]interface{}) - require.Contains(t, mountCounts, "non_entity_clients", "expected mount counts to contain a non_entity_clients key") - mountCountNonEntityRaw := mountCounts["non_entity_clients"].(json.Number) + require.Contains(t, mountCounts, "acme_clients", "expected mount counts to contain a non_entity_clients key") + mountCountNonEntityRaw := mountCounts["acme_clients"].(json.Number) mountCountNonEntity, err := mountCountNonEntityRaw.Int64() require.NoError(t, err, "failed to parse number as int64: "+mountCountNonEntityRaw.String()) require.Greater(t, mountCountNonEntity, int64(0), "expected at least one non-entity client count value in the mount") diff --git a/changelog/26020.txt b/changelog/26020.txt new file mode 100644 index 000000000000..5ce91856bf32 --- /dev/null +++ b/changelog/26020.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core/activity: Include ACME clients in activity log responses +``` diff --git a/vault/activity/query.go b/vault/activity/query.go index 7e035c3dd7fb..ea95c3ae1ea7 100644 --- a/vault/activity/query.go +++ b/vault/activity/query.go @@ -24,17 +24,19 @@ type NamespaceRecord struct { NonEntityTokens uint64 `json:"non_entity_tokens"` SecretSyncs uint64 `json:"secret_syncs"` Mounts []*MountRecord `json:"mounts"` + ACMEClients uint64 `json:"acme_clients"` } type CountsRecord struct { EntityClients int `json:"entity_clients"` NonEntityClients int `json:"non_entity_clients"` SecretSyncs int `json:"secret_syncs"` + ACMEClients int `json:"acme_clients"` } // HasCounts returns true when any of the record's fields have a non-zero value func (c *CountsRecord) HasCounts() bool { - return c.EntityClients+c.NonEntityClients+c.SecretSyncs != 0 + return c.EntityClients+c.NonEntityClients+c.SecretSyncs+c.ACMEClients != 0 } type NewClientRecord struct { diff --git a/vault/activity_log.go b/vault/activity_log.go index 869a2cff5318..cb6d833ae5b7 100644 --- a/vault/activity_log.go +++ b/vault/activity_log.go @@ -87,6 +87,8 @@ const ( secretSyncActivityType = "secret-sync" ) +var ActivityClientTypes = []string{nonEntityTokenActivityType, entityActivityType, secretSyncActivityType, ACMEActivityType} + type segmentInfo struct { startTimestamp int64 currentClients *activity.EntityActivityLog @@ -1573,6 +1575,7 @@ type ResponseCounts struct { NonEntityClients int `json:"non_entity_clients" mapstructure:"non_entity_clients"` Clients int `json:"clients"` SecretSyncs int `json:"secret_syncs" mapstructure:"secret_syncs"` + ACMEClients int `json:"acme_clients" mapstructure:"acme_clients"` } // Add adds the new record's counts to the existing record @@ -1585,6 +1588,7 @@ func (r *ResponseCounts) Add(newRecord *ResponseCounts) { r.DistinctEntities += newRecord.DistinctEntities r.NonEntityClients += newRecord.NonEntityClients r.NonEntityTokens += newRecord.NonEntityTokens + r.ACMEClients += newRecord.ACMEClients r.SecretSyncs += newRecord.SecretSyncs } @@ -2035,6 +2039,7 @@ func (p *processCounts) toCountsRecord() *activity.CountsRecord { return &activity.CountsRecord{ EntityClients: p.countByType(entityActivityType), NonEntityClients: p.countByType(nonEntityTokenActivityType), + ACMEClients: p.countByType(ACMEActivityType), SecretSyncs: p.countByType(secretSyncActivityType), } } @@ -2045,7 +2050,7 @@ func (p *processCounts) toCountsRecord() *activity.CountsRecord { func (p *processCounts) countByType(typ string) int { switch typ { case nonEntityTokenActivityType: - return len(p.ClientsByType[nonEntityTokenActivityType]) + int(p.Tokens) + len(p.ClientsByType[ACMEActivityType]) + return len(p.ClientsByType[nonEntityTokenActivityType]) + int(p.Tokens) } return len(p.ClientsByType[typ]) } @@ -2053,17 +2058,6 @@ func (p *processCounts) countByType(typ string) int { // clientsByType returns the set of client IDs with the given type. // ACME clients are included in the non entity results func (p *processCounts) clientsByType(typ string) clientIDSet { - switch typ { - case nonEntityTokenActivityType: - clients := make(clientIDSet) - for c := range p.ClientsByType[nonEntityTokenActivityType] { - clients[c] = struct{}{} - } - for c := range p.ClientsByType[ACMEActivityType] { - clients[c] = struct{}{} - } - return clients - } return p.ClientsByType[typ] } @@ -2792,6 +2786,7 @@ func (a *ActivityLog) partialMonthClientCount(ctx context.Context) (map[string]i responseData["non_entity_clients"] = totalCounts.NonEntityClients responseData["clients"] = totalCounts.Clients responseData["secret_syncs"] = totalCounts.SecretSyncs + responseData["acme_clients"] = totalCounts.ACMEClients // The partialMonthClientCount should not have more than one month worth of data. // If it does, something has gone wrong and we should warn that the activity log data diff --git a/vault/activity_log_test.go b/vault/activity_log_test.go index 84d14c4b92aa..1f81402122c9 100644 --- a/vault/activity_log_test.go +++ b/vault/activity_log_test.go @@ -4219,14 +4219,14 @@ func TestActivityLog_partialMonthClientCountWithMultipleMountPaths(t *testing.T) // TestActivityLog_processNewClients_delete ensures that the correct clients are deleted from a processNewClients struct func TestActivityLog_processNewClients_delete(t *testing.T) { mount := "mount" - namespace := "namespace" + ns := "namespace" clientID := "client-id" run := func(t *testing.T, clientType string) { t.Helper() isNonEntity := clientType == nonEntityTokenActivityType || clientType == ACMEActivityType record := &activity.EntityRecord{ MountAccessor: mount, - NamespaceID: namespace, + NamespaceID: ns, ClientID: clientID, NonEntity: isNonEntity, ClientType: clientType, @@ -4235,8 +4235,8 @@ func TestActivityLog_processNewClients_delete(t *testing.T) { newClients.add(record) require.True(t, newClients.Counts.contains(record)) - require.True(t, newClients.Namespaces[namespace].Counts.contains(record)) - require.True(t, newClients.Namespaces[namespace].Mounts[mount].Counts.contains(record)) + require.True(t, newClients.Namespaces[ns].Counts.contains(record)) + require.True(t, newClients.Namespaces[ns].Mounts[mount].Counts.contains(record)) newClients.delete(record) @@ -4244,8 +4244,8 @@ func TestActivityLog_processNewClients_delete(t *testing.T) { counts := newClients.Counts for _, typ := range []string{nonEntityTokenActivityType, secretSyncActivityType, entityActivityType, ACMEActivityType} { require.NotContains(t, counts.clientsByType(typ), clientID) - require.NotContains(t, byNS[namespace].Mounts[mount].Counts.clientsByType(typ), clientID) - require.NotContains(t, byNS[namespace].Counts.clientsByType(typ), clientID) + require.NotContains(t, byNS[ns].Mounts[mount].Counts.clientsByType(typ), clientID) + require.NotContains(t, byNS[ns].Counts.clientsByType(typ), clientID) } } t.Run("entity", func(t *testing.T) { @@ -4267,14 +4267,15 @@ func TestActivityLog_processNewClients_delete(t *testing.T) { func TestActivityLog_processClientRecord(t *testing.T) { startTime := time.Now() mount := "mount" - namespace := "namespace" + ns := "namespace" clientID := "client-id" + run := func(t *testing.T, clientType string) { t.Helper() isNonEntity := clientType == nonEntityTokenActivityType || clientType == ACMEActivityType record := &activity.EntityRecord{ MountAccessor: mount, - NamespaceID: namespace, + NamespaceID: ns, ClientID: clientID, NonEntity: isNonEntity, ClientType: clientType, @@ -4282,28 +4283,28 @@ func TestActivityLog_processClientRecord(t *testing.T) { byNS := make(summaryByNamespace) byMonth := make(summaryByMonth) processClientRecord(record, byNS, byMonth, startTime) - require.Contains(t, byNS, namespace) - require.Contains(t, byNS[namespace].Mounts, mount) + require.Contains(t, byNS, ns) + require.Contains(t, byNS[ns].Mounts, mount) monthIndex := timeutil.StartOfMonth(startTime).UTC().Unix() require.Contains(t, byMonth, monthIndex) require.Equal(t, byMonth[monthIndex].Namespaces, byNS) require.Equal(t, byMonth[monthIndex].NewClients.Namespaces, byNS) - for _, typ := range []string{nonEntityTokenActivityType, secretSyncActivityType, entityActivityType} { - if clientType == typ || (clientType == ACMEActivityType && typ == nonEntityTokenActivityType) { + for _, typ := range ActivityClientTypes { + if clientType == typ { require.Contains(t, byMonth[monthIndex].Counts.clientsByType(typ), clientID) require.Contains(t, byMonth[monthIndex].NewClients.Counts.clientsByType(typ), clientID) - require.Contains(t, byNS[namespace].Mounts[mount].Counts.clientsByType(typ), clientID) - require.Contains(t, byNS[namespace].Counts.clientsByType(typ), clientID) + require.Contains(t, byNS[ns].Mounts[mount].Counts.clientsByType(typ), clientID) + require.Contains(t, byNS[ns].Counts.clientsByType(typ), clientID) } else { require.NotContains(t, byMonth[monthIndex].Counts.clientsByType(typ), clientID) require.NotContains(t, byMonth[monthIndex].NewClients.Counts.clientsByType(typ), clientID) - require.NotContains(t, byNS[namespace].Mounts[mount].Counts.clientsByType(typ), clientID) - require.NotContains(t, byNS[namespace].Counts.clientsByType(typ), clientID) - + require.NotContains(t, byNS[ns].Mounts[mount].Counts.clientsByType(typ), clientID) + require.NotContains(t, byNS[ns].Counts.clientsByType(typ), clientID) } } } + t.Run("non entity", func(t *testing.T) { run(t, nonEntityTokenActivityType) }) @@ -4600,6 +4601,12 @@ func TestActivityLog_writePrecomputedQuery(t *testing.T) { MountAccessor: "mnt-3", ClientType: secretSyncActivityType, } + acmeClient := &activity.EntityRecord{ + ClientID: "id-4", + NamespaceID: "ns-4", + MountAccessor: "mnt-4", + ClientType: ACMEActivityType, + } now := time.Now() @@ -4607,6 +4614,7 @@ func TestActivityLog_writePrecomputedQuery(t *testing.T) { processClientRecord(clientEntity, byNS, byMonth, now) processClientRecord(clientNonEntity, byNS, byMonth, now) processClientRecord(secretSync, byNS, byMonth, now) + processClientRecord(acmeClient, byNS, byMonth, now) endTime := timeutil.EndOfMonth(now) opts := pqOptions{ @@ -4624,8 +4632,8 @@ func TestActivityLog_writePrecomputedQuery(t *testing.T) { require.Equal(t, now.UTC().Unix(), val.StartTime.UTC().Unix()) require.Equal(t, endTime.UTC().Unix(), val.EndTime.UTC().Unix()) - // ns-1, ns-2, and ns-3 should both be present in the results - require.Len(t, val.Namespaces, 3) + // ns-1, ns-2, ns-3, ns-4 should all be present in the results + require.Len(t, val.Namespaces, 4) require.Len(t, val.Months, 1) resultByNS := make(map[string]*activity.NamespaceRecord) for _, ns := range val.Namespaces { @@ -4634,41 +4642,62 @@ func TestActivityLog_writePrecomputedQuery(t *testing.T) { ns1 := resultByNS["ns-1"] ns2 := resultByNS["ns-2"] ns3 := resultByNS["ns-3"] + ns4 := resultByNS["ns-4"] require.Equal(t, ns1.Entities, uint64(1)) require.Equal(t, ns1.NonEntityTokens, uint64(0)) require.Equal(t, ns1.SecretSyncs, uint64(0)) + require.Equal(t, ns1.ACMEClients, uint64(0)) require.Equal(t, ns2.Entities, uint64(0)) require.Equal(t, ns2.NonEntityTokens, uint64(1)) require.Equal(t, ns2.SecretSyncs, uint64(0)) + require.Equal(t, ns2.ACMEClients, uint64(0)) require.Equal(t, ns3.Entities, uint64(0)) require.Equal(t, ns3.NonEntityTokens, uint64(0)) require.Equal(t, ns3.SecretSyncs, uint64(1)) + require.Equal(t, ns3.ACMEClients, uint64(0)) + require.Equal(t, ns4.Entities, uint64(0)) + require.Equal(t, ns4.NonEntityTokens, uint64(0)) + require.Equal(t, ns4.SecretSyncs, uint64(0)) + require.Equal(t, ns4.ACMEClients, uint64(1)) require.Len(t, ns1.Mounts, 1) require.Len(t, ns2.Mounts, 1) require.Len(t, ns3.Mounts, 1) + require.Len(t, ns4.Mounts, 1) + // ns-1 needs to have mnt-1 require.Contains(t, ns1.Mounts[0].MountPath, "mnt-1") // ns-2 needs to have mnt-2 require.Contains(t, ns2.Mounts[0].MountPath, "mnt-2") // ns-3 needs to have mnt-3 require.Contains(t, ns3.Mounts[0].MountPath, "mnt-3") + // ns-4 needs to have mnt-4 + require.Contains(t, ns4.Mounts[0].MountPath, "mnt-4") // ns1 only has an entity client require.Equal(t, 1, ns1.Mounts[0].Counts.EntityClients) require.Equal(t, 0, ns1.Mounts[0].Counts.NonEntityClients) require.Equal(t, 0, ns1.Mounts[0].Counts.SecretSyncs) + require.Equal(t, 0, ns1.Mounts[0].Counts.ACMEClients) // ns2 only has a non entity client require.Equal(t, 0, ns2.Mounts[0].Counts.EntityClients) require.Equal(t, 1, ns2.Mounts[0].Counts.NonEntityClients) require.Equal(t, 0, ns2.Mounts[0].Counts.SecretSyncs) + require.Equal(t, 0, ns2.Mounts[0].Counts.ACMEClients) // ns3 only has a secret sync association require.Equal(t, 0, ns3.Mounts[0].Counts.EntityClients) require.Equal(t, 0, ns3.Mounts[0].Counts.NonEntityClients) require.Equal(t, 1, ns3.Mounts[0].Counts.SecretSyncs) + require.Equal(t, 0, ns3.Mounts[0].Counts.ACMEClients) + + // ns4 only has an ACME client + require.Equal(t, 0, ns4.Mounts[0].Counts.EntityClients) + require.Equal(t, 0, ns4.Mounts[0].Counts.NonEntityClients) + require.Equal(t, 0, ns4.Mounts[0].Counts.SecretSyncs) + require.Equal(t, 1, ns4.Mounts[0].Counts.ACMEClients) monthRecord := val.Months[0] // there should only be one month present, since the clients were added with the same timestamp @@ -4676,11 +4705,13 @@ func TestActivityLog_writePrecomputedQuery(t *testing.T) { require.Equal(t, 1, monthRecord.Counts.NonEntityClients) require.Equal(t, 1, monthRecord.Counts.EntityClients) require.Equal(t, 1, monthRecord.Counts.SecretSyncs) - require.Len(t, monthRecord.Namespaces, 3) - require.Len(t, monthRecord.NewClients.Namespaces, 3) + require.Equal(t, 1, monthRecord.Counts.ACMEClients) + require.Len(t, monthRecord.Namespaces, 4) + require.Len(t, monthRecord.NewClients.Namespaces, 4) require.Equal(t, 1, monthRecord.NewClients.Counts.EntityClients) require.Equal(t, 1, monthRecord.NewClients.Counts.NonEntityClients) require.Equal(t, 1, monthRecord.NewClients.Counts.SecretSyncs) + require.Equal(t, 1, monthRecord.NewClients.Counts.ACMEClients) } type mockTimeNowClock struct { @@ -4759,9 +4790,9 @@ func TestAddActivityToFragment(t *testing.T) { a.SetEnable(true) mount := "mount" - namespace := "root" + ns := "root" id := "id1" - a.AddActivityToFragment(id, namespace, 0, entityActivityType, mount) + a.AddActivityToFragment(id, ns, 0, entityActivityType, mount) testCases := []struct { name string @@ -4810,13 +4841,14 @@ func TestAddActivityToFragment(t *testing.T) { isNonEntity: true, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { a.fragmentLock.RLock() numClientsBefore := len(a.fragment.Clients) a.fragmentLock.RUnlock() - a.AddActivityToFragment(tc.id, namespace, 0, tc.activityType, mount) + a.AddActivityToFragment(tc.id, ns, 0, tc.activityType, mount) a.fragmentLock.RLock() defer a.fragmentLock.RUnlock() numClientsAfter := len(a.fragment.Clients) @@ -4830,7 +4862,7 @@ func TestAddActivityToFragment(t *testing.T) { require.Contains(t, a.partialMonthClientTracker, tc.expectedID) require.True(t, proto.Equal(&activity.EntityRecord{ ClientID: tc.expectedID, - NamespaceID: namespace, + NamespaceID: ns, Timestamp: 0, NonEntity: tc.isNonEntity, MountAccessor: mount, diff --git a/vault/activity_log_util_common.go b/vault/activity_log_util_common.go index 152da93a9744..952e06ead46b 100644 --- a/vault/activity_log_util_common.go +++ b/vault/activity_log_util_common.go @@ -115,13 +115,11 @@ func (a *ActivityLog) computeCurrentMonthForBillingPeriodInternal(ctx context.Co return nil, errors.New(fmt.Sprintf("multiple months of data found in partial month's client count breakdowns: %+v\n", byMonth)) } - activityTypes := []string{entityActivityType, nonEntityTokenActivityType, secretSyncActivityType} - // Now we will add the clients for the current month to a copy of the billing period's hll to // see how the cardinality grows. - hllByType := make(map[string]*hyperloglog.Sketch, len(activityTypes)) - totalByType := make(map[string]int, len(activityTypes)) - for _, typ := range activityTypes { + hllByType := make(map[string]*hyperloglog.Sketch, len(ActivityClientTypes)) + totalByType := make(map[string]int, len(ActivityClientTypes)) + for _, typ := range ActivityClientTypes { hllByType[typ] = billingPeriodHLL.Clone() } @@ -130,7 +128,7 @@ func (a *ActivityLog) computeCurrentMonthForBillingPeriodInternal(ctx context.Co return nil, errors.New("malformed current month used to calculate current month's activity") } - for _, typ := range activityTypes { + for _, typ := range ActivityClientTypes { // Note that the following calculations assume that all clients seen are currently in // the NewClients section of byMonth. It is best to explicitly check this, just verify // our assumptions about the passed in byMonth argument. @@ -146,8 +144,9 @@ func (a *ActivityLog) computeCurrentMonthForBillingPeriodInternal(ctx context.Co } } } - currentMonthNewByType := make(map[string]int, len(activityTypes)) - for _, typ := range activityTypes { + + currentMonthNewByType := make(map[string]int, len(ActivityClientTypes)) + for _, typ := range ActivityClientTypes { // The number of new entities for the current month is approximately the size of the hll with // the current month's entities minus the size of the initial billing period hll. currentMonthNewByType[typ] = int(hllByType[typ].Estimate() - billingPeriodHLL.Estimate()) @@ -159,11 +158,13 @@ func (a *ActivityLog) computeCurrentMonthForBillingPeriodInternal(ctx context.Co EntityClients: currentMonthNewByType[entityActivityType], NonEntityClients: currentMonthNewByType[nonEntityTokenActivityType], SecretSyncs: currentMonthNewByType[secretSyncActivityType], + ACMEClients: currentMonthNewByType[ACMEActivityType], }}, Counts: &activity.CountsRecord{ EntityClients: totalByType[entityActivityType], NonEntityClients: totalByType[nonEntityTokenActivityType], SecretSyncs: totalByType[secretSyncActivityType], + ACMEClients: totalByType[ACMEActivityType], }, }, nil } @@ -188,6 +189,7 @@ func (a *ActivityLog) transformALNamespaceBreakdowns(nsData map[string]*processB Entities: uint64(ns.Counts.countByType(entityActivityType)), NonEntityTokens: uint64(ns.Counts.countByType(nonEntityTokenActivityType)), SecretSyncs: uint64(ns.Counts.countByType(secretSyncActivityType)), + ACMEClients: uint64(ns.Counts.countByType(ACMEActivityType)), Mounts: a.transformActivityLogMounts(ns.Mounts), } byNamespace = append(byNamespace, &nsRecord) @@ -390,8 +392,9 @@ func (a *ActivityLog) countsRecordToCountsResponse(record *activity.CountsRecord response := &ResponseCounts{ EntityClients: record.EntityClients, NonEntityClients: record.NonEntityClients, - Clients: record.EntityClients + record.NonEntityClients + record.SecretSyncs, + Clients: record.EntityClients + record.NonEntityClients + record.SecretSyncs + record.ACMEClients, SecretSyncs: record.SecretSyncs, + ACMEClients: record.ACMEClients, } if includeDeprecated { response.NonEntityTokens = response.NonEntityClients @@ -409,7 +412,8 @@ func (a *ActivityLog) namespaceRecordToCountsResponse(record *activity.Namespace EntityClients: int(record.Entities), NonEntityTokens: int(record.NonEntityTokens), NonEntityClients: int(record.NonEntityTokens), - Clients: int(record.Entities + record.NonEntityTokens + record.SecretSyncs), + Clients: int(record.Entities + record.NonEntityTokens + record.SecretSyncs + record.ACMEClients), SecretSyncs: int(record.SecretSyncs), + ACMEClients: int(record.ACMEClients), } }