forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Register a Telemetry handler for basic Canvas usage stats (elastic#823)
Closes https://github.com/elastic/kibana-canvas/issues/128 Makes a summary of Canvas usage that looks like: ``` { "kibana_canvas": { "elements": { "per_page": { "avg": 2, "max": 3, "min": 1 }, "total": 4 }, "functions": { "in_use": [ "filters", "demodata", "markdown", "render", "image", "pointseries", "plot", "getCell", "repeatImage" ], "per_element": { "avg": 4, "max": 5, "min": 2 }, "total": 16 }, "pages": { "per_workpad": { "avg": 1, "max": 1, "min": 1 }, "total": 2 }, "workpads": { "total": 2 } } } ``` To complete end-to-end Telemetry functionality, More work is going on with Monitoring and Telemetry to have every registered collector automatically include the their data in the overall payload. There's ongoing work to implement that in core Kibana, but none of it blocks the PR from being merged. - Having the Canvas usage stats automatically added to `.monitoring-kibana-*` documents depends on this PR: elastic#22030 - That PR can be pulled to help test this PR. The result will be Canvas usage stats in the `kibana_stats` documents in `.monitoring-kibana-*`. - Having Canvas usage stats automatically included in the telemetry payload that gets sent to Elastic depends on this unstarted issue: elastic#21239 - Without that additional work, the way to test this PR is to check for the `usage.kibana_canvas` data in the`localhost:5601/api/stats?extended=true` API response. That pending work in Kibana core does not block this PR from getting merged.
- Loading branch information
Showing
7 changed files
with
352 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
export const workpads = [ | ||
{ | ||
pages: [ | ||
{ | ||
elements: [ | ||
{ | ||
expression: ` | ||
demodata | | ||
ply by=age fn={rowCount | as count} | | ||
staticColumn total value={math 'sum(count)'} | | ||
mapColumn percentage fn={math 'count/total * 100'} | | ||
sort age | | ||
pointseries x=age y=percentage | | ||
plot defaultStyle={seriesStyle points=0 lines=5}`, | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
{ | ||
pages: [{ elements: [{ expression: 'filters | demodata | markdown "hello" | render' }] }], | ||
}, | ||
{ | ||
pages: [ | ||
{ | ||
elements: [ | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'filters | demodata | markdown "hello" | render' }, | ||
{ expression: 'filters | demodata | pointseries | pie | render' }, | ||
], | ||
}, | ||
{ elements: [{ expression: 'filters | demodata | table | render' }] }, | ||
{ elements: [{ expression: 'image | render' }] }, | ||
{ elements: [{ expression: 'image | render' }] }, | ||
], | ||
}, | ||
{ | ||
pages: [ | ||
{ | ||
elements: [ | ||
{ expression: 'filters | demodata | markdown "hello" | render' }, | ||
{ expression: 'filters | demodata | markdown "hello" | render' }, | ||
{ expression: 'image | render' }, | ||
], | ||
}, | ||
{ | ||
elements: [ | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'filters | demodata | markdown "hello" | render' }, | ||
{ expression: 'filters | demodata | pointseries | pie | render' }, | ||
{ expression: 'image | render' }, | ||
], | ||
}, | ||
{ | ||
elements: [ | ||
{ expression: 'filters | demodata | pointseries | pie | render' }, | ||
{ | ||
expression: | ||
'filters | demodata | pointseries | plot defaultStyle={seriesStyle points=0 lines=5} | render', | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
{ | ||
pages: [ | ||
{ | ||
elements: [ | ||
{ expression: 'demodata | render as=debug' }, | ||
{ expression: 'filters | demodata | pointseries | plot | render' }, | ||
{ expression: 'filters | demodata | table | render' }, | ||
{ expression: 'filters | demodata | table | render' }, | ||
], | ||
}, | ||
{ | ||
elements: [ | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'filters | demodata | pointseries | pie | render' }, | ||
{ expression: 'image | render' }, | ||
], | ||
}, | ||
{ | ||
elements: [ | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'demodata | render as=debug' }, | ||
{ expression: 'shape "square" | render' }, | ||
], | ||
}, | ||
], | ||
}, | ||
{ | ||
pages: [ | ||
{ | ||
elements: [ | ||
{ expression: 'demodata | pointseries | getCell | repeatImage | render' }, | ||
{ expression: 'filters | demodata | markdown "hello" | render' }, | ||
], | ||
}, | ||
{ elements: [{ expression: 'image | render' }] }, | ||
{ elements: [{ expression: 'image | render' }] }, | ||
{ elements: [{ expression: 'filters | demodata | table | render' }] }, | ||
], | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import expect from 'expect.js'; | ||
import { handleResponse } from '../collector'; | ||
import { workpads } from '../../../__tests__/fixtures/workpads'; | ||
|
||
const getMockResponse = (mocks = workpads) => ({ | ||
hits: { | ||
hits: mocks.map(workpad => ({ | ||
_source: { | ||
'canvas-workpad': workpad, | ||
}, | ||
})), | ||
}, | ||
}); | ||
|
||
describe('usage collector handle es response data', () => { | ||
it('should summarize workpads, pages, and elements', () => { | ||
const usage = handleResponse(getMockResponse()); | ||
expect(usage).to.eql({ | ||
workpads: { | ||
total: 6, // num workpad documents in .kibana index | ||
}, | ||
pages: { | ||
total: 16, // num pages in all the workpads | ||
per_workpad: { avg: 2.6666666666666665, min: 1, max: 4 }, | ||
}, | ||
elements: { | ||
total: 34, // num elements in all the pages | ||
per_page: { avg: 2.125, min: 1, max: 5 }, | ||
}, | ||
functions: { | ||
per_element: { avg: 4, min: 2, max: 7 }, | ||
total: 36, | ||
in_use: [ | ||
'demodata', | ||
'ply', | ||
'rowCount', | ||
'as', | ||
'staticColumn', | ||
'math', | ||
'mapColumn', | ||
'sort', | ||
'pointseries', | ||
'plot', | ||
'seriesStyle', | ||
'filters', | ||
'markdown', | ||
'render', | ||
'getCell', | ||
'repeatImage', | ||
'pie', | ||
'table', | ||
'image', | ||
'shape', | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
it('should fail gracefully if workpad has 0 pages (corrupted workpad)', () => { | ||
const mockEsResponseCorrupted = getMockResponse([ | ||
{ | ||
name: 'Tweet Data Workpad', | ||
id: 'workpad-ae00567f-5510-4d68-b07f-6b1661948e03', | ||
width: 792, | ||
height: 612, | ||
page: 0, | ||
pages: [], // pages should never be empty, and *may* prevent the ui from rendering properly | ||
'@timestamp': '2018-07-26T02:29:00.964Z', | ||
'@created': '2018-07-25T22:56:31.460Z', | ||
assets: {}, | ||
}, | ||
]); | ||
const usage = handleResponse(mockEsResponseCorrupted); | ||
expect(usage).to.eql({ | ||
workpads: { total: 1 }, | ||
pages: { total: 0, per_workpad: { avg: 0, min: 0, max: 0 } }, | ||
elements: undefined, | ||
functions: undefined, | ||
}); | ||
}); | ||
|
||
it('should fail gracefully in general', () => { | ||
const usage = handleResponse({ hits: { total: 0 } }); | ||
expect(usage).to.eql(undefined); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { sum as arraySum, min as arrayMin, max as arrayMax, get } from 'lodash'; | ||
import { CANVAS_USAGE_TYPE, CANVAS_TYPE } from '../../common/lib/constants'; | ||
import { fromExpression } from '../../common/lib/ast'; | ||
|
||
/* | ||
* @param ast: an ast that includes functions to track | ||
* @param cb: callback to do something with a function that has been found | ||
*/ | ||
const collectFns = (ast, cb) => { | ||
if (ast.type === 'expression') { | ||
ast.chain.forEach(({ function: cFunction, arguments: cArguments }) => { | ||
cb(cFunction); | ||
|
||
// recurse the argumetns and update the set along the way | ||
Object.keys(cArguments).forEach(argName => { | ||
cArguments[argName].forEach(subAst => { | ||
collectFns(subAst, cb); | ||
}); | ||
}); | ||
}); | ||
} | ||
}; | ||
|
||
export function handleResponse({ hits }) { | ||
const workpadDocs = get(hits, 'hits', null); | ||
if (workpadDocs == null) { | ||
return; | ||
} | ||
|
||
const functionSet = new Set(); | ||
|
||
// make a summary of info about each workpad | ||
const workpadsInfo = workpadDocs.map(hit => { | ||
const workpad = hit._source[CANVAS_TYPE]; | ||
|
||
let pages; | ||
try { | ||
pages = { count: workpad.pages.length }; | ||
} catch (err) { | ||
console.warn(err, workpad); | ||
} | ||
const elementCounts = workpad.pages.reduce( | ||
(accum, page) => accum.concat(page.elements.length), | ||
[] | ||
); | ||
const functionCounts = workpad.pages.reduce((accum, page) => { | ||
return page.elements.map(element => { | ||
const ast = fromExpression(element.expression); | ||
collectFns(ast, cFunction => { | ||
functionSet.add(cFunction); | ||
}); | ||
return ast.chain.length; // get the number of parts in the expression | ||
}); | ||
}, []); | ||
return { pages, elementCounts, functionCounts }; | ||
}); | ||
|
||
// combine together info from across the workpads | ||
const combinedWorkpadsInfo = workpadsInfo.reduce( | ||
(accum, pageInfo) => { | ||
const { pages, elementCounts, functionCounts } = pageInfo; | ||
|
||
return { | ||
pageMin: pages.count < accum.pageMin ? pages.count : accum.pageMin, | ||
pageMax: pages.count > accum.pageMax ? pages.count : accum.pageMax, | ||
pageCounts: accum.pageCounts.concat(pages.count), | ||
elementCounts: accum.elementCounts.concat(elementCounts), | ||
functionCounts: accum.functionCounts.concat(functionCounts), | ||
}; | ||
}, | ||
{ | ||
pageMin: Infinity, | ||
pageMax: -Infinity, | ||
pageCounts: [], | ||
elementCounts: [], | ||
functionCounts: [], | ||
} | ||
); | ||
const { pageCounts, pageMin, pageMax, elementCounts, functionCounts } = combinedWorkpadsInfo; | ||
|
||
const pageTotal = arraySum(pageCounts); | ||
const elementsTotal = arraySum(elementCounts); | ||
const functionsTotal = arraySum(functionCounts); | ||
const pagesInfo = | ||
workpadsInfo.length > 0 | ||
? { | ||
total: pageTotal, | ||
per_workpad: { | ||
avg: pageTotal / pageCounts.length, | ||
min: pageMin, | ||
max: pageMax, | ||
}, | ||
} | ||
: undefined; | ||
const elementsInfo = | ||
pageTotal > 0 | ||
? { | ||
total: elementsTotal, | ||
per_page: { | ||
avg: elementsTotal / elementCounts.length, | ||
min: arrayMin(elementCounts), | ||
max: arrayMax(elementCounts), | ||
}, | ||
} | ||
: undefined; | ||
const functionsInfo = | ||
elementsTotal > 0 | ||
? { | ||
total: functionsTotal, | ||
in_use: Array.from(functionSet), | ||
per_element: { | ||
avg: functionsTotal / functionCounts.length, | ||
min: arrayMin(functionCounts), | ||
max: arrayMax(functionCounts), | ||
}, | ||
} | ||
: undefined; | ||
|
||
return { | ||
workpads: { total: workpadsInfo.length }, | ||
pages: pagesInfo, | ||
elements: elementsInfo, | ||
functions: functionsInfo, | ||
}; | ||
} | ||
|
||
export function registerCanvasUsageCollector(server) { | ||
const index = server.config().get('kibana.index'); | ||
const collector = server.usage.collectorSet.makeUsageCollector({ | ||
type: CANVAS_USAGE_TYPE, | ||
fetch: async callCluster => { | ||
const searchParams = { | ||
size: 10000, // elasticsearch index.max_result_window default value | ||
index, | ||
ignoreUnavailable: true, | ||
filterPath: ['hits.hits._source.canvas-workpad'], | ||
body: { query: { term: { type: { value: CANVAS_TYPE } } } }, | ||
}; | ||
|
||
const esResponse = await callCluster('search', searchParams); | ||
if (get(esResponse, 'hits.hits.length') > 0) { | ||
return handleResponse(esResponse); | ||
} | ||
}, | ||
}); | ||
|
||
server.usage.collectorSet.register(collector); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { registerCanvasUsageCollector } from './collector'; |