Skip to content

Commit

Permalink
Register a Telemetry handler for basic Canvas usage stats (elastic#823)
Browse files Browse the repository at this point in the history
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
tsullivan authored and w33ble committed Aug 22, 2018
1 parent 9fa513b commit 1dcb872
Show file tree
Hide file tree
Showing 7 changed files with 352 additions and 0 deletions.
6 changes: 6 additions & 0 deletions __tests__/fixtures/kibana.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export class Plugin {
has: key => has(config, key),
}),
route: def => this.routes.push(def),
usage: {
collectorSet: {
makeUsageCollector: () => {},
register: () => {},
},
},
};

const { init } = this.props;
Expand Down
106 changes: 106 additions & 0 deletions __tests__/fixtures/workpads.js
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' }] },
],
},
];
1 change: 1 addition & 0 deletions common/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const API_ROUTE = '/api/canvas';
export const API_ROUTE_WORKPAD = `${API_ROUTE}/workpad`;
export const LOCALSTORAGE_LASTPAGE = 'canvas:lastpage';
export const FETCH_TIMEOUT = 30000; // 30 seconds
export const CANVAS_USAGE_TYPE = 'canvas';
4 changes: 4 additions & 0 deletions init.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { typesRegistry } from './common/lib/types_registry';
import { serverFunctions } from './server/functions';
import { commonFunctions } from './common/functions';
import { typeSpecs } from './common/types';
import { registerCanvasUsageCollector } from './server/usage';

export default function(server /*options*/) {
server.plugins.canvas = {
Expand Down Expand Up @@ -45,4 +46,7 @@ export default function(server /*options*/) {

// Load routes here
routes(server);

// Register a usage collector with Telemetry service
registerCanvasUsageCollector(server);
}
86 changes: 86 additions & 0 deletions server/usage/__tests__/collector.handle_response.js
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);
});
});
148 changes: 148 additions & 0 deletions server/usage/collector.js
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);
}
1 change: 1 addition & 0 deletions server/usage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { registerCanvasUsageCollector } from './collector';

0 comments on commit 1dcb872

Please sign in to comment.