Skip to content
This repository has been archived by the owner on Aug 9, 2022. It is now read-only.

Add frontend metrics for Kibana reports #277

Merged
merged 5 commits into from
Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions kibana-reports/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import registerReportRoute from './report';
import registerReportDefinitionRoute from './reportDefinition';
import registerReportSourceRoute from './reportSource';
import registerMetricRoute from './metric';
import { IRouter } from '../../../../src/core/server';

export default function (router: IRouter) {
registerReportRoute(router);
registerReportDefinitionRoute(router);
registerReportSourceRoute(router);
registerMetricRoute(router);
}
49 changes: 49 additions & 0 deletions kibana-reports/server/routes/metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import {
IKibanaResponse,
IRouter,
ResponseError,
} from '../../../../src/core/server';
import { API_PREFIX } from '../../common';
import { errorResponse } from './utils/helpers';
import { getMetrics } from './utils/metricHelper';

export default function (router: IRouter) {
router.get(
{
path: `${API_PREFIX}/stats`,
validate: false,
},
async (
context,
request,
response
): Promise<IKibanaResponse<any | ResponseError>> => {
//@ts-ignore
const logger: Logger = context.reporting_plugin.logger;
try {
const metrics = getMetrics();
return response.ok({
body: metrics,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np: could simplify to body: getMetrics()?

});
} catch (error) {
logger.error(`failed during query reporting stats: ${error}`);
return errorResponse(response, error);
}
}
);
}
26 changes: 24 additions & 2 deletions kibana-reports/server/routes/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
backendToUiReport,
backendToUiReportsList,
} from './utils/converters/backendToUi';
import { addToMetric } from './utils/metricHelper';

export default function (router: IRouter) {
// generate report (with provided metadata)
Expand All @@ -51,13 +52,15 @@ export default function (router: IRouter) {
//@ts-ignore
const logger: Logger = context.reporting_plugin.logger;
let report = request.body;
addToMetric(report, 'count');
// input validation
try {
report.report_definition.report_params.core_params.origin =
request.headers.origin;
report = reportSchema.validate(report);
} catch (error) {
logger.error(`Failed input validation for create report ${error}`);
addToMetric(report, 'customer_error');
return response.badRequest({ body: error });
}

Expand All @@ -83,6 +86,11 @@ export default function (router: IRouter) {
// TODO: better error handling for delivery and stages in generating report, pass logger to deeper level
logger.error(`Failed to generate report: ${error}`);
logger.error(error);
if (error.statusCode && Math.floor(error.statusCode / 100) === 4) {
addToMetric(report, 'customer_error');
} else {
addToMetric(report, 'system_error');
}
return errorResponse(response, error);
}
}
Expand All @@ -108,6 +116,7 @@ export default function (router: IRouter) {
): Promise<IKibanaResponse<any | ResponseError>> => {
//@ts-ignore
const logger: Logger = context.reporting_plugin.logger;
let report: any;
try {
const savedReportId = request.params.reportId;
// @ts-ignore
Expand All @@ -122,7 +131,8 @@ export default function (router: IRouter) {
}
);
// convert report to use UI model
const report = backendToUiReport(esResp.reportInstance);
report = backendToUiReport(esResp.reportInstance);
addToMetric(report, 'count');
// generate report
const reportData = await createReport(
request,
Expand All @@ -140,6 +150,11 @@ export default function (router: IRouter) {
} catch (error) {
logger.error(`Failed to generate report by id: ${error}`);
logger.error(error);
if (error.statusCode && Math.floor(error.statusCode / 100) === 4) {
addToMetric(report, 'customer_error');
} else {
addToMetric(report, 'system_error');
}
return errorResponse(response, error);
}
}
Expand All @@ -166,6 +181,7 @@ export default function (router: IRouter) {
//@ts-ignore
const logger: Logger = context.reporting_plugin.logger;
const reportDefinitionId = request.params.reportDefinitionId;
let report: any;
try {
// @ts-ignore
const esReportsClient: ILegacyScopedClusterClient = context.reporting_plugin.esReportsClient.asScoped(
Expand All @@ -183,7 +199,8 @@ export default function (router: IRouter) {
);
const reportId = esResp.reportInstance.id;
// convert report to use UI model
const report = backendToUiReport(esResp.reportInstance);
report = backendToUiReport(esResp.reportInstance);
addToMetric(report, 'count');
// generate report
const reportData = await createReport(
request,
Expand All @@ -203,6 +220,11 @@ export default function (router: IRouter) {
`Failed to generate report from reportDefinition id ${reportDefinitionId} : ${error}`
);
logger.error(error);
if (error.statusCode && Math.floor(error.statusCode / 100) === 4) {
addToMetric(report, 'customer_error');
} else {
addToMetric(report, 'system_error');
}
return errorResponse(response, error);
}
}
Expand Down
9 changes: 7 additions & 2 deletions kibana-reports/server/routes/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
* permissions and limitations under the License.
*/

import { PLUGIN_ID } from '../../../common';

export enum FORMAT {
pdf = 'pdf',
png = 'png',
Expand Down Expand Up @@ -80,3 +78,10 @@ export const DEFAULT_REPORT_HEADER = '<h1>Open Distro Kibana Reports</h1>';
export const SECURITY_AUTH_COOKIE_NAME = 'security_authentication';

export const CHROMIUM_PATH = `${__dirname}/../../../.chromium/headless_shell`;

/**
* Metrics setting constants
*/
export const WINDOW = 3600;
export const INTERVAL = 60;
export const CAPACITY = (WINDOW / INTERVAL) * 2;
188 changes: 188 additions & 0 deletions kibana-reports/server/routes/utils/metricHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

import { ReportSchemaType } from 'server/model';
import {
BasicCounterType,
RollingCountersNameType,
RollingCountersType,
} from './types';
import _ from 'lodash';
import { CAPACITY, INTERVAL, WINDOW } from './constants';

export let time2CountWin: Map<number, RollingCountersType> = new Map();
let globalBasicCounter: BasicCounterType = {
zhongnansu marked this conversation as resolved.
Show resolved Hide resolved
dashboard: {
pdf: {
download: {
total: 0,
},
},
png: {
download: {
total: 0,
},
},
},
visualization: {
pdf: {
download: {
total: 0,
},
},
png: {
download: {
total: 0,
},
},
},
saved_search: {
csv: {
download: {
total: 0,
},
},
},
};

let defaultRollingCounter: RollingCountersType = {
dashboard: {
pdf: {
download: {
count: 0,
system_error: 0,
customer_error: 0,
},
},
png: {
download: {
count: 0,
system_error: 0,
customer_error: 0,
},
},
},
visualization: {
pdf: {
download: {
count: 0,
system_error: 0,
customer_error: 0,
},
},
png: {
download: {
count: 0,
system_error: 0,
customer_error: 0,
},
},
},
saved_search: {
csv: {
download: {
count: 0,
system_error: 0,
customer_error: 0,
},
},
},
};

export const addToMetric = (
report: ReportSchemaType,
field: RollingCountersNameType
) => {
const count = 1;
// remove outdated key-value pairs
trim();

const timeKey = getKey(Date.now());
const rollingCounters = time2CountWin.get(timeKey);
rollingCounters
? time2CountWin.set(
timeKey,
updateCounters(report, field, rollingCounters, count)
)
: time2CountWin.set(
timeKey,
updateCounters(report, field, _.cloneDeep(defaultRollingCounter), count)
);
zhongnansu marked this conversation as resolved.
Show resolved Hide resolved
};

export const getMetrics = () => {
const preTimeKey = getPreKey(Date.now());
const rollingCounters = time2CountWin.get(preTimeKey);
const metrics = buildMetrics(rollingCounters, globalBasicCounter);
return metrics;
};

const trim = () => {
if (time2CountWin.size > CAPACITY) {
time2CountWin.forEach((_value, key, map) => {
if (key < getKey(Date.now() - WINDOW * 1000)) {
zhongnansu marked this conversation as resolved.
Show resolved Hide resolved
map.delete(key);
}
});
}
};

const getKey = (milliseconds: number) => {
return Math.floor(milliseconds / 1000 / INTERVAL);
};

const getPreKey = (milliseconds: number) => {
return getKey(milliseconds) - 1;
};

const buildMetrics = (
rollingCounters: RollingCountersType | undefined,
basicCounters: BasicCounterType
) => {
if (!rollingCounters) {
rollingCounters = defaultRollingCounter;
}
return _.merge(rollingCounters, basicCounters);
};

const updateCounters = (
report: ReportSchemaType,
field: RollingCountersNameType,
rollingCounter: RollingCountersType,
count: number
) => {
const {
report_definition: {
report_params: {
report_source: source,
core_params: { report_format: format },
},
},
} = report;

// @ts-ignore
rollingCounter[source.toLowerCase().replace(' ', '_')][format]['download'][
field
] += count;
//update basic counter for total
if (field === 'count') {
//@ts-ignore
globalBasicCounter[source.toLowerCase().replace(' ', '_')][format][
'download'
]['total']++;
}

return rollingCounter;
};
28 changes: 28 additions & 0 deletions kibana-reports/server/routes/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,31 @@ export interface CreateReportResultType {
dataUrl: string;
fileName: string;
}

type ReportSource = 'dashboard' | 'visualization' | 'saved_search';
type ReportFormat = 'pdf' | 'png' | 'csv';
type UserActionType = 'download';
export type RollingCountersNameType =
| 'count'
| 'system_error'
| 'customer_error';

export type RollingCountersType = {
[source in ReportSource]: {
[format in ReportFormat]?: {
[action in UserActionType]: {
[counter in RollingCountersNameType]: number;
};
};
};
};

export type BasicCounterType = {
[source in ReportSource]: {
[format in ReportFormat]?: {
[action in UserActionType]: {
total: number;
};
};
};
};