[Usage counters] Relocate counters to a dedicated index. Unify / simplify 'ui' and 'server' counters logic.#187064
Conversation
There was a problem hiding this comment.
The UsageCountersService should ensure that a certain domainId is not registered more than once (line 163)
afharo
left a comment
There was a problem hiding this comment.
Overall looks like a great and much-needed refactor of our counters system. Moving them to a custom index will give us the opportunity to scale. Also, moving UI Counters to actually persist the properties in a 1:1 mapping instead of : splits is another great win.
However, I have some reservations with the shared API dealing with multiple SOs: Moving UI Counters to its own SO type is a great move to decouple UI and Server Counters' codebases. Except that it doesn't really decouple them: both SO types are registered from a common place.
We are storing them separately, but a consumer of the API can't create a counter with the same domainId in the server and the browser (which it's a pain since domainId tends to be the plugin ID in most usages, like dashboards).
I wonder if UI counters should prepend ui: to the domainId so that these clashes won't occur. This could also keep the SO type hidden from the public API.
Alternatively, we could specify source: 'ui' | 'server' to hide the persistence layer (SO type). And, if we need to specify the source during the usage counter getOrCreate, I wonder if it could just be one property in a common SO type, that we can filter by. Crazy use case to back it up:
Count views of dashboards coming from the UI (source: 'ui'), and the API (source: 'server').
WDYT?
...kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.test.ts
Outdated
Show resolved
Hide resolved
...gins/kibana_usage_collection/server/collectors/ui_counters/register_ui_counters_collector.ts
Outdated
Show resolved
Hide resolved
..._usage_collection/server/collectors/usage_counters/register_usage_counters_collector.test.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Q: why would it fail in a way that retries could help? AFAIK, there's no I/O here.
IMO, it's enough with
const counter = counters.getUsageCounterByDomainId(domainId) ??
counters.createUsageCounter(domainId, UI_COUNTERS_SAVED_OBJECT_TYPE);There was a problem hiding this comment.
I had Java and context switching in my head, you're right.
src/plugins/usage_collection/server/usage_counters/saved_objects.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
do we want the fallback here? If we fallback here, we're essentially making no distinction for the default space, aren't we?
There was a problem hiding this comment.
In the end I'm not creating any agnostic SO types, so they all need to be stored in a specific namespace.
The idea is that whenever users of the API don't specify a namespace, the default one will be used.
There was a problem hiding this comment.
🚨 mind to revert this when you're done testing :)
There was a problem hiding this comment.
Oops good catch!
src/plugins/usage_collection/server/usage_counters/usage_counters_service.ts
Outdated
Show resolved
Hide resolved
src/plugins/usage_collection/server/usage_counters/usage_counters_service.ts
Outdated
Show resolved
Hide resolved
...ges/core/saved-objects/core-saved-objects-migration-server-internal/src/core/unused_types.ts
Outdated
Show resolved
Hide resolved
src/plugins/usage_collection/server/usage_counters/saved_objects.ts
Outdated
Show resolved
Hide resolved
src/plugins/kibana_usage_collection/server/collectors/common/counters.ts
Outdated
Show resolved
Hide resolved
|
Answering comment After discussion, we've decided to use a single so type
|
…' Vs 'server'. Address PR feedback.
…kibana_usage_counters`
f835bfa to
fff9e1c
Compare
| // Check for migration steps present in the logs | ||
| logs = await fs.readFile(logFilePath, 'utf-8'); | ||
| expect(logs).not.toMatch('CREATE_NEW_TARGET'); | ||
| expect(logs).not.toMatch('[.kibana] CREATE_NEW_TARGET'); |
There was a problem hiding this comment.
The test now creates a new index for the new type.
I made the constraint more specific and ensure that a new .kibana is not being created.
|
|
||
| expect(Array.isArray(typesMap['.kibana'])).toEqual(true); | ||
| expect(typesMap['.kibana'].length > 50).toEqual(true); | ||
| expect(typesMap['.kibana'].includes('action')).toEqual(true); |
There was a problem hiding this comment.
Checking that the map contains an exhaustive list of mappings is not necessary.
This test is broken everytime anyone adds a new SO type.
I've relaxed the constraint a bit.
|
|
||
| // the deleteByNamespace performs an updateByQuery under the hood. | ||
| // It targets all SO indices that have namespace-aware types | ||
| const nsIndices = getIndicesWithNamespaceAwareTypes(setup.savedObjects.getTypeRegistry()); |
There was a problem hiding this comment.
Previously we were running a very costly operation, registering thousands of routes, when 99% of requests target .kibana solely.
The only case where multiple indices were specified in the HTTP request path is with namespace-aware indices.
The test was timing out due to the new SO index, which added a lot more permutations.
Hence, I used the opportunity to simplify it.
The test should now be noticeably ligher/faster.
| /** | ||
| * Parameters to the `serializeCounterKey` method | ||
| * @internal used in kibana_usage_collectors | ||
| * @internal bused in kibana_usage_collectors |
| createMockSavedObjectDoc( | ||
| moment().subtract(6, 'days'), | ||
| 'doc-id-3', | ||
| USAGE_COUNTERS_SAVED_OBJECT_TYPE |
💛 Build succeeded, but was flaky
Failed CI StepsTest Failures
Metrics [docs]Public APIs missing comments
Unknown metric groupsAPI count
History
To update your PR or re-run it, just comment with: |
|
Pinging @elastic/kibana-core (Team:Core) |
| return { dailyEvents }; | ||
| } | ||
| const UI: UsageCounters.v1.CounterEventSource = 'ui'; | ||
| const UI_COUNTERS_FILTER = `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.source: ${UI}`; |
There was a problem hiding this comment.
Do we need the .attributes bit? I thought the core logic introduced it automatically
There was a problem hiding this comment.
We do. the core rewrite is {type}.attributes.{attrPath} => {type}.{attrPath} (as each type's attributes are stored under a property matching their name, for historical reasons)
|
|
||
| return await Promise.all( | ||
| docsToDelete.map(({ id }) => savedObjectsClient.delete(USAGE_COUNTERS_SAVED_OBJECT_TYPE, id)) | ||
| docsToDelete.map(({ id, type }) => savedObjectsClient.delete(type, id)) |
There was a problem hiding this comment.
Q: do we need to specify the namespace when deleting it
There was a problem hiding this comment.
Q2: Should we take this opportunity to use bulkDelete?
There was a problem hiding this comment.
As discussed on slack: because we're using an unscoped repository, we can (and need to) pass the namespace when calling delete
(and yeah, using bulkDelete would be ideal)
There was a problem hiding this comment.
Will update to take namespaces into account.
Unfortunately bulkDelete does not allow for a multi-namespace delete operation, and I believe it relies on the spaces extension.
There was a problem hiding this comment.
Overall LGTM.
Only mandatory change is adapting the delete calls (#187064 (comment))
| "tokenType" | ||
| ], | ||
| "core-usage-stats": [], | ||
| "counter": [ |
There was a problem hiding this comment.
I wonder if we shouldn't go with something slightly less generic than just counter for the type identifier? core-counters at least? Maybe with an additional modifier if we're planning on having multiple SO types for our counter needs?
WDYT?
There was a problem hiding this comment.
I'll update it to usage-counter (it's not a core feature).
We finally have a single type and store both agnostic and namespace-aware under namespaceType: single, using default when no namespace is provided.
| namespaces: ['*'], | ||
| fields: ['count', 'counterName', 'counterType', 'domainId'], | ||
| filter, | ||
| perPage: 1000, |
There was a problem hiding this comment.
NIT: (I doubt it will ever be relevant, but still) one of the upsidesof the PIT is to release memory preasure by having smaller batches of fetched objects, so we usually want to have smaller batches than 1k (like 100 or so).
| dailyEvents.push(event); | ||
| } | ||
| } catch (_) { | ||
| // swallow error; allows sending successfully transformed objects. |
There was a problem hiding this comment.
NIT: we don't even want a warning or debug level message here?
| acc[key] = { | ||
| ...existingCounter, | ||
| ...counter, | ||
| total: existingCounter.total + counter.total, |
There was a problem hiding this comment.
Is there any scenario where the spreading properties of existingCounter and counter will differ?
My question is, could this simply be replaced by:
acc[key].total = acc[key].total + counter.total? (just minor perf increase, avoiding 2 spreads + an object creation)
There was a problem hiding this comment.
The only property we want to systematically override is lastUpdatedAt with the most recent one.
I'll update the code with your perf. enhancement.
| return { dailyEvents }; | ||
| } | ||
| const UI: UsageCounters.v1.CounterEventSource = 'ui'; | ||
| const UI_COUNTERS_FILTER = `${USAGE_COUNTERS_SAVED_OBJECT_TYPE}.attributes.source: ${UI}`; |
There was a problem hiding this comment.
We do. the core rewrite is {type}.attributes.{attrPath} => {type}.{attrPath} (as each type's attributes are stored under a property matching their name, for historical reasons)
|
|
||
| return await Promise.all( | ||
| docsToDelete.map(({ id }) => savedObjectsClient.delete(USAGE_COUNTERS_SAVED_OBJECT_TYPE, id)) | ||
| docsToDelete.map(({ id, type }) => savedObjectsClient.delete(type, id)) |
There was a problem hiding this comment.
As discussed on slack: because we're using an unscoped repository, we can (and need to) pass the namespace when calling delete
(and yeah, using bulkDelete would be ideal)
| * Creates and registers a usage counter to collect daily aggregated plugin counter events | ||
| */ | ||
| createUsageCounter: (type: string) => UsageCounter; | ||
| createUsageCounter: (type: string, source?: 'server' | 'ui') => UsageCounter; |
There was a problem hiding this comment.
NIT: how much work would it represen to have this additional parameter be mandatory? How many calls to adapt?
There was a problem hiding this comment.
In fact this is wrong/outdated.
The counters will not be bound to a source anymore. The source will be specified in the events.
createUsageCounter only reserves a certain 'domainId', but it does not register / persist any counter on that domain. Perhaps it should be named registerUsageCounterDomain instead.
| import * as Rx from 'rxjs'; | ||
| import * as rxOp from 'rxjs'; |
There was a problem hiding this comment.
NIT: unrelated to the PR. but 🙃
| serializeCounterKey({ ...attributes, date: updatedAt, namespace: namespaces?.[0] }) | ||
| ) | ||
| .sort() | ||
| ).toEqual(RECENT_COUNTERS); |
There was a problem hiding this comment.
Added an integration test to ensure we delete old ones, across namespaces, but preserve recent ones (even when their keys match those of the old ones).
… src/core/server/integration_tests/ci_checks'
|
Took a look at the latest changes - restamping with LGTM |
💛 Build succeeded, but was flaky
Failed CI StepsMetrics [docs]Public APIs missing comments
Unknown metric groupsAPI count
History
|
## Summary Follow up to #187064 * Improves the `rollups.test.ts` integration test. * Adds the `count` field to the mappings, so that it can be aggregated.
## Summary Part of #186530 Follow-up of #187064 The goal of this PR is to provide the necessary means to allow implementing the [Counting views](https://docs.google.com/document/d/1W77qoweixcjrq0sEKh_LjIk3j33Xyy9umod9mG9BlOM/edit) part of the _Dashboards++_ initiative. We do this by extending the capabilities of the _usage counters_ APIs: * We support custom retention periods. Currently data is only kept in SO indices for 5 days. Having 90 days worth of counting was required for Dashboards++. * We expose a Search API that will allow retrieving persisted counters. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>

Summary
Part of #186530.
This PR sets the basis for allowing namespaced usage counters.
It relocates
usage-countersSO type from.kibanato a dedicated.kibana_usage_counters.Furthermore, the original SO type is removed, and replaced by 2 separate types:
server-countersui-countersNote that these 2 steps are necessary if we want to leverage
namespacesproperty of the saved objects.We can't currently update the
namespaceType: 'agnostic'without causing a migration.Thus, these two types will be defined as
namespaceType: single.Up until now, UI counters were stored under a special
domainId: uiCounter.This forced a workaround that consisted in storing
appName:eventNamein thecounterNameproperty of the SO.Having a dedicated SO type for them allows to store
appNameasdomainId, avoiding the need for a workaround.