-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds telemetry support to alerting and actions plugins #58081
Changes from all commits
6980b46
41aedb9
3e7dee2
e422da5
e9a101c
82e42d8
5d38139
491c7df
aafa6c1
7ac9035
6a236af
90202df
62af6b8
b799870
c2ceb31
edef1a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { APICaller } from 'kibana/server'; | ||
|
||
export async function getTotalCount(callCluster: APICaller, kibanaIndex: string) { | ||
const scriptedMetric = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm slightly surprised we can't use some existing agg here, instead of having to script it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Existing aggregations is designed to work on the document fields, but in case it is a child document like a relation or alert.actions list - we should use nested path and in this case we still able to aggregate only fields on the nested level. I did a deep research over documentation and didn't find any other option https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-aggregations-bucket-nested-aggregation.html. The most pain here that we don't have an actionTypeId in 'relations', but have it on alert.actions and both of this objects are the separate branches of nesting which is not accessible for each other. |
||
scripted_metric: { | ||
init_script: 'state.types = [:]', | ||
map_script: ` | ||
String actionType = doc['action.actionTypeId'].value; | ||
state.types.put(actionType, state.types.containsKey(actionType) ? state.types.get(actionType) + 1 : 1); | ||
`, | ||
// Combine script is executed per cluster, but we already have a key-value pair per cluster. | ||
// Despite docs that say this is optional, this script can't be blank. | ||
combine_script: 'return state', | ||
// Reduce script is executed across all clusters, so we need to add up all the total from each cluster | ||
// This also needs to account for having no data | ||
reduce_script: ` | ||
Map result = [:]; | ||
for (Map m : states.toArray()) { | ||
if (m !== null) { | ||
for (String k : m.keySet()) { | ||
result.put(k, result.containsKey(k) ? result.get(k) + m.get(k) : m.get(k)); | ||
} | ||
} | ||
} | ||
return result; | ||
`, | ||
}, | ||
}; | ||
|
||
const searchResult = await callCluster('search', { | ||
index: kibanaIndex, | ||
rest_total_hits_as_int: true, | ||
body: { | ||
query: { | ||
bool: { | ||
filter: [{ term: { type: 'action' } }], | ||
}, | ||
}, | ||
aggs: { | ||
byActionTypeId: scriptedMetric, | ||
}, | ||
}, | ||
}); | ||
|
||
return { | ||
countTotal: Object.keys(searchResult.aggregations.byActionTypeId.value.types).reduce( | ||
(total: number, key: string) => | ||
parseInt(searchResult.aggregations.byActionTypeId.value.types[key], 0) + total, | ||
0 | ||
), | ||
countByType: searchResult.aggregations.byActionTypeId.value.types, | ||
}; | ||
} | ||
|
||
export async function getInUseTotalCount(callCluster: APICaller, kibanaIndex: string) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like we could probably do all these aggs in a single search, but I doubt there's a great reason to do that, since they won't be running all that often. And it no doubt easier to deal with a small set of semi-complex searchs, rather than run a single search with a bunch of semi-complex aggs in it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had the same thoughts, when was writing this code, but decided to keep it separately for now. |
||
const scriptedMetric = { | ||
scripted_metric: { | ||
init_script: 'state.connectorIds = new HashMap(); state.total = 0;', | ||
map_script: ` | ||
String connectorId = doc['references.id'].value; | ||
String actionRef = doc['references.name'].value; | ||
if (state.connectorIds[connectorId] === null) { | ||
state.connectorIds[connectorId] = actionRef; | ||
state.total++; | ||
} | ||
`, | ||
// Combine script is executed per cluster, but we already have a key-value pair per cluster. | ||
// Despite docs that say this is optional, this script can't be blank. | ||
combine_script: 'return state', | ||
// Reduce script is executed across all clusters, so we need to add up all the total from each cluster | ||
// This also needs to account for having no data | ||
reduce_script: ` | ||
Map connectorIds = [:]; | ||
long total = 0; | ||
for (state in states) { | ||
if (state !== null) { | ||
total += state.total; | ||
for (String k : state.connectorIds.keySet()) { | ||
connectorIds.put(k, connectorIds.containsKey(k) ? connectorIds.get(k) + state.connectorIds.get(k) : state.connectorIds.get(k)); | ||
} | ||
} | ||
} | ||
Map result = new HashMap(); | ||
result.total = total; | ||
result.connectorIds = connectorIds; | ||
return result; | ||
`, | ||
}, | ||
}; | ||
|
||
const actionResults = await callCluster('search', { | ||
index: kibanaIndex, | ||
rest_total_hits_as_int: true, | ||
body: { | ||
query: { | ||
bool: { | ||
filter: { | ||
bool: { | ||
must: { | ||
nested: { | ||
path: 'references', | ||
query: { | ||
bool: { | ||
filter: { | ||
bool: { | ||
must: [ | ||
{ | ||
term: { | ||
'references.type': 'action', | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
aggs: { | ||
refs: { | ||
nested: { | ||
path: 'references', | ||
}, | ||
aggs: { | ||
actionRefIds: scriptedMetric, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
return actionResults.aggregations.refs.actionRefIds.value.total; | ||
} | ||
|
||
// TODO: Implement executions count telemetry with eventLog, when it will write to index |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; | ||
import { registerActionsUsageCollector } from './actions_usage_collector'; | ||
import { taskManagerMock } from '../../../task_manager/server/task_manager.mock'; | ||
|
||
const mockTaskManagerStart = taskManagerMock.start(); | ||
|
||
beforeEach(() => jest.resetAllMocks()); | ||
|
||
describe('registerActionsUsageCollector', () => { | ||
let usageCollectionMock: jest.Mocked<UsageCollectionSetup>; | ||
beforeEach(() => { | ||
usageCollectionMock = ({ | ||
makeUsageCollector: jest.fn(), | ||
registerCollector: jest.fn(), | ||
} as unknown) as jest.Mocked<UsageCollectionSetup>; | ||
}); | ||
|
||
it('should call registerCollector', () => { | ||
registerActionsUsageCollector(usageCollectionMock, mockTaskManagerStart); | ||
expect(usageCollectionMock.registerCollector).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
it('should call makeUsageCollector with type = actions', () => { | ||
registerActionsUsageCollector(usageCollectionMock, mockTaskManagerStart); | ||
expect(usageCollectionMock.makeUsageCollector).toHaveBeenCalledTimes(1); | ||
expect(usageCollectionMock.makeUsageCollector.mock.calls[0][0].type).toBe('actions'); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; | ||
import { get } from 'lodash'; | ||
import { TaskManagerStartContract } from '../../../task_manager/server'; | ||
import { ActionsUsage } from './types'; | ||
|
||
export function createActionsUsageCollector( | ||
usageCollection: UsageCollectionSetup, | ||
taskManager: TaskManagerStartContract | ||
) { | ||
return usageCollection.makeUsageCollector({ | ||
type: 'actions', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so does |
||
isReady: () => true, | ||
fetch: async (): Promise<ActionsUsage> => { | ||
try { | ||
const doc = await getLatestTaskState(await taskManager); | ||
// get the accumulated state from the recurring task | ||
const state: ActionsUsage = get(doc, 'state'); | ||
|
||
return { | ||
...state, | ||
}; | ||
} catch (err) { | ||
return { | ||
mikecote marked this conversation as resolved.
Show resolved
Hide resolved
|
||
count_total: 0, | ||
count_active_total: 0, | ||
count_active_by_type: {}, | ||
count_by_type: {}, | ||
}; | ||
} | ||
}, | ||
}); | ||
} | ||
|
||
async function getLatestTaskState(taskManager: TaskManagerStartContract) { | ||
try { | ||
const result = await taskManager.get('Actions-actions_telemetry'); | ||
return result; | ||
} catch (err) { | ||
const errMessage = err && err.message ? err.message : err.toString(); | ||
/* | ||
The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the | ||
task manager has to wait for all plugins to initialize first. It's fine to ignore it as next time around it will be | ||
initialized (or it will throw a different type of error) | ||
*/ | ||
if (!errMessage.includes('NotInitialized')) { | ||
throw err; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
export function registerActionsUsageCollector( | ||
usageCollection: UsageCollectionSetup, | ||
taskManager: TaskManagerStartContract | ||
) { | ||
const collector = createActionsUsageCollector(usageCollection, taskManager); | ||
usageCollection.registerCollector(collector); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
export { registerActionsUsageCollector } from './actions_usage_collector'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh neat, didn't realized you could get start services (async) in setup!