Instrument Kibana with APM RUM agent (#44281)

* Instrument Kibana with APM RUM agent

* make route-change transaction work with properl url

* extract page-load transaction url from app link

* check if app is hidden and set active:false

* make distributed tracing work and merge config

* remove config/apm.js and address review

* address review comments

* add apm.js to build tassks

* move apm from dev to src

* add @types/hoist-non-react-statics which is required by react rum

* apply changes correctly from master
This commit is contained in:
Vignesh Shanmugam 2019-12-18 12:16:15 +01:00 committed by GitHub
parent 8863fc29a4
commit 254b18c20b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 227 additions and 103 deletions

1
.gitignore vendored
View file

@ -29,7 +29,6 @@ disabledPlugins
webpackstats.json
/config/*
!/config/kibana.yml
!/config/apm.js
coverage
selenium
.babel_register_cache.json

View file

@ -1,87 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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.
*/
/**
* DO NOT EDIT THIS FILE!
*
* This file contains the configuration for the Elastic APM instrumentaion of
* Kibana itself and is only intented to be used during development of Kibana.
*
* Instrumentation is turned off by default. Once activated it will send APM
* data to an Elasticsearch cluster accessible by Elastic employees.
*
* To modify the configuration, either use environment variables, or create a
* file named `config/apm.dev.js`, which exports a config object as described
* in the docs.
*
* For an overview over the available configuration files, see:
* https://www.elastic.co/guide/en/apm/agent/nodejs/current/configuration.html
*
* For general information about Elastic APM, see:
* https://www.elastic.co/guide/en/apm/get-started/current/index.html
*/
const { readFileSync } = require('fs');
const { join } = require('path');
const { execSync } = require('child_process');
const merge = require('lodash.merge');
module.exports = merge(
{
active: false,
serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443',
// The secretToken below is intended to be hardcoded in this file even though
// it makes it public. This is not a security/privacy issue. Normally we'd
// instead disable the need for a secretToken in the APM Server config where
// the data is transmitted to, but due to how it's being hosted, it's easier,
// for now, to simply leave it in.
secretToken: 'R0Gjg46pE9K9wGestd',
globalLabels: {},
centralConfig: false,
logUncaughtExceptions: true,
},
devConfig()
);
const rev = gitRev();
if (rev !== null) module.exports.globalLabels.git_rev = rev;
try {
const filename = join(__dirname, '..', 'data', 'uuid');
module.exports.globalLabels.kibana_uuid = readFileSync(filename, 'utf-8');
} catch (e) {} // eslint-disable-line no-empty
function gitRev() {
try {
return execSync('git rev-parse --short HEAD', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch (e) {
return null;
}
}
function devConfig() {
try {
return require('./apm.dev'); // eslint-disable-line import/no-unresolved
} catch (e) {
return {};
}
}

View file

@ -112,6 +112,7 @@
"dependencies": {
"@babel/core": "^7.5.5",
"@babel/register": "^7.7.0",
"@elastic/apm-rum": "^4.6.0",
"@elastic/charts": "^14.0.0",
"@elastic/datemath": "5.0.2",
"@elastic/ems-client": "1.0.5",

View file

@ -17,21 +17,81 @@
* under the License.
*/
const { existsSync } = require('fs');
const { join } = require('path');
const { name, version } = require('../package.json');
const { readFileSync } = require('fs');
const { execSync } = require('child_process');
const merge = require('lodash.merge');
const { name, version, build } = require('../package.json');
const ROOT_DIR = join(__dirname, '..');
function gitRev() {
try {
return execSync('git rev-parse --short HEAD', {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch (e) {
return null;
}
}
function devConfig() {
try {
const apmDevConfigPath = join(ROOT_DIR, 'config', 'apm.dev.js');
return require(apmDevConfigPath); // eslint-disable-line import/no-dynamic-require
} catch (e) {
return {};
}
}
const apmConfig = merge(
{
active: false,
serverUrl: 'https://f1542b814f674090afd914960583265f.apm.us-central1.gcp.cloud.es.io:443',
// The secretToken below is intended to be hardcoded in this file even though
// it makes it public. This is not a security/privacy issue. Normally we'd
// instead disable the need for a secretToken in the APM Server config where
// the data is transmitted to, but due to how it's being hosted, it's easier,
// for now, to simply leave it in.
secretToken: 'R0Gjg46pE9K9wGestd',
globalLabels: {},
breakdownMetrics: true,
centralConfig: false,
logUncaughtExceptions: true,
},
devConfig()
);
try {
const filename = join(ROOT_DIR, 'data', 'uuid');
apmConfig.globalLabels.kibana_uuid = readFileSync(filename, 'utf-8');
} catch (e) {} // eslint-disable-line no-empty
const rev = gitRev();
if (rev !== null) apmConfig.globalLabels.git_rev = rev;
function getConfig(serviceName) {
return {
...apmConfig,
...{
serviceName: `${serviceName}-${version.replace(/\./g, '_')}`,
},
};
}
/**
* Flag to disable APM RUM support on all kibana builds by default
*/
const isKibanaDistributable = Boolean(build && build.distributable === true);
module.exports = function(serviceName = name) {
if (process.env.kbnWorkerType === 'optmzr') return;
const conf = {
serviceName: `${serviceName}-${version.replace(/\./g, '_')}`,
};
const configFile = join(__dirname, '..', 'config', 'apm.js');
if (existsSync(configFile)) conf.configFile = configFile;
else conf.active = false;
const conf = getConfig(serviceName);
require('elastic-apm-node').start(conf);
};
module.exports.getConfig = getConfig;
module.exports.isKibanaDistributable = isKibanaDistributable;

View file

@ -80,6 +80,9 @@ export interface InjectedMetadataParams {
user?: Record<string, UserProvidedValues>;
};
};
apm: {
[key: string]: unknown;
};
};
}

View file

@ -46,7 +46,6 @@ export const CopySourceTask = {
'typings/**',
'webpackShims/**',
'config/kibana.yml',
'config/apm.js',
'tsconfig*.json',
'.i18nrc.json',
'kibana.d.ts',

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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 { getConfig, isKibanaDistributable } from '../../../apm';
import agent from 'elastic-apm-node';
const apmEnabled = !isKibanaDistributable && process.env.ELASTIC_APM_ACTIVE === 'true';
export function apmImport() {
return apmEnabled ? 'import { init } from "@elastic/apm-rum"' : '';
}
export function apmInit(config) {
return apmEnabled ? `init(${config})` : '';
}
export function getApmConfig(appMetadata) {
if (!apmEnabled) {
return {};
}
/**
* we use the injected app metadata from the server to extract the
* app URL path to be used for page-load transaction
*/
const navLink = appMetadata.getNavLink();
const pageUrl = navLink ? navLink.toJSON().url : appMetadata._url;
const config = {
...getConfig('kibana-frontend'),
...{
active: true,
pageLoadTransactionName: pageUrl,
},
};
/**
* Get current active backend transaction to make distrubuted tracing
* work for rendering the app
*/
const backendTransaction = agent.currentTransaction;
if (backendTransaction) {
const { sampled, traceId } = backendTransaction;
return {
...config,
...{
pageLoadTraceId: traceId,
pageLoadSampled: sampled,
pageLoadSpanId: backendTransaction.ensureParentId(),
},
};
}
return config;
}

View file

@ -56,7 +56,23 @@ export default function RouteManager() {
}
};
self.run = function($location, $route, $injector) {
self.run = function($location, $route, $injector, $rootScope) {
if (window.elasticApm && typeof window.elasticApm.startTransaction === 'function') {
/**
* capture route-change events as transactions which happens after
* the browser's on load event.
*
* In Kibana app, this logic would run after the boostrap js files gets
* downloaded and get associated with the page-load transaction
*/
$rootScope.$on('$routeChangeStart', (_, nextRoute) => {
if (nextRoute.$$route) {
const name = nextRoute.$$route.originalPath;
window.elasticApm.startTransaction(name, 'route-change');
}
});
}
self.getBreadcrumbs = () => {
const breadcrumbs = parsePathToBreadcrumbs($location.path());
const map = $route.current.mapBreadcrumbs;

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { apmImport, apmInit } from '../apm';
export const appEntryTemplate = bundle => `
/**
* Kibana entry file
@ -34,12 +36,14 @@ import 'custom-event-polyfill';
import 'whatwg-fetch';
import 'abortcontroller-polyfill';
import 'childnode-remove-polyfill';
${apmImport()}
import { i18n } from '@kbn/i18n';
import { CoreSystem } from '__kibanaCore__'
const injectedMetadata = JSON.parse(document.querySelector('kbn-injected-metadata').getAttribute('data'));
${apmInit('injectedMetadata.apm')}
i18n.load(injectedMetadata.i18n.translationsUrl)
.catch(e => e)
.then((i18nError) => {

View file

@ -28,6 +28,7 @@ import { AppBootstrap } from './bootstrap';
import { mergeVariables } from './lib';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { fromRoot } from '../../../core/server/utils';
import { getApmConfig } from '../apm';
export function uiRenderMixin(kbnServer, server, config) {
function replaceInjectedVars(request, injectedVars) {
@ -282,6 +283,8 @@ export function uiRenderMixin(kbnServer, server, config) {
uiPlugins,
legacyMetadata,
apm: getApmConfig(legacyMetadata.app),
},
});

View file

@ -7,6 +7,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Route, Router, Switch } from 'react-router-dom';
import { ApmRoute } from '@elastic/apm-rum-react';
import styled from 'styled-components';
import { metadata } from 'ui/metadata';
import {
@ -52,7 +53,7 @@ const App = () => {
<Route component={ScrollToTopOnPathChange} />
<Switch>
{routes.map((route, i) => (
<Route key={i} {...route} />
<ApmRoute key={i} {...route} />
))}
</Switch>
</MainContainer>

View file

@ -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.
*/
declare module '@elastic/apm-rum-react';

View file

@ -66,6 +66,7 @@
"@types/graphql": "^0.13.2",
"@types/gulp": "^4.0.6",
"@types/hapi__wreck": "^15.0.1",
"@types/hoist-non-react-statics": "^3.3.0",
"@types/history": "^4.7.3",
"@types/jest": "24.0.19",
"@types/joi": "^13.4.2",
@ -175,6 +176,7 @@
"@babel/core": "^7.5.5",
"@babel/register": "^7.7.0",
"@babel/runtime": "^7.5.5",
"@elastic/apm-rum-react": "^0.3.2",
"@elastic/datemath": "5.0.2",
"@elastic/ems-client": "1.0.5",
"@elastic/eui": "17.0.0",

View file

@ -1336,6 +1336,31 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@elastic/apm-rum-core@^4.7.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-4.7.0.tgz#b00b58bf7380f2e36652e5333e3ca97608986e40"
integrity sha512-/lTZWfA3ces3qoKCx72Sc+w43lZkyktaQlbYoYO86h3tNX7tScc/7YBBHI9oxKMcXweqkKOcpnwNZFy71bb86w==
dependencies:
error-stack-parser "^1.3.5"
es6-promise "^4.2.8"
opentracing "^0.14.3"
uuid "^3.1.0"
"@elastic/apm-rum-react@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-0.3.2.tgz#134634643e15ebcf97b6f17b2c74a50afdbe1c64"
integrity sha512-hU1srW9noygppyrLmipulu30c+LWEie8V/dQjEqLYMx2mRZRwNIue3midYgWa6qrWqgYZhwpAtWrWcXc+AWk2Q==
dependencies:
"@elastic/apm-rum" "^4.6.0"
hoist-non-react-statics "^3.3.0"
"@elastic/apm-rum@^4.6.0":
version "4.6.0"
resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-4.6.0.tgz#e2ac560dd4a4761c0e9b08c301418b1d4063bdd2"
integrity sha512-hsqvyTm5rT6lKgV06wvm8ID9aMsuJyw8wIOPjRwKmvzlTjayabxKTcr50lJJV8jY9OWfDkqymIqpHyCEChQAHQ==
dependencies:
"@elastic/apm-rum-core" "^4.7.0"
"@elastic/charts@^14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-14.0.0.tgz#410c87e9ae53df5848aae09a210fa7d08510b376"
@ -3609,7 +3634,7 @@
resolved "https://registry.yarnpkg.com/@types/hoek/-/hoek-4.1.3.tgz#d1982d48fb0d2a0e5d7e9d91838264d8e428d337"
integrity sha1-0ZgtSPsNKg5dfp2Rg4Jk2OQo0zc=
"@types/hoist-non-react-statics@*":
"@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
@ -11205,6 +11230,13 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
error-stack-parser@^1.3.5:
version "1.3.6"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-1.3.6.tgz#e0e73b93e417138d1cd7c0b746b1a4a14854c292"
integrity sha1-4Oc7k+QXE40c18C3RrGkoUhUwpI=
dependencies:
stackframe "^0.3.1"
error-stack-parser@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.4.tgz#a757397dc5d9de973ac9a5d7d4e8ade7cfae9101"
@ -11308,6 +11340,11 @@ es6-promise@^4.2.5:
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
@ -21014,6 +21051,11 @@ opener@^1.4.2:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.1.tgz#6d2f0e77f1a0af0032aca716c2c1fbb8e7e8abed"
integrity sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==
opentracing@^0.14.3:
version "0.14.4"
resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.4.tgz#a113408ea740da3a90fde5b3b0011a375c2e4268"
integrity sha512-nNnZDkUNExBwEpb7LZaeMeQgvrlO8l4bgY/LvGNZCR0xG/dGWqHqjKrAmR5GUoYo0FIz38kxasvA1aevxWs2CA==
opn@^5.3.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.4.0.tgz#cb545e7aab78562beb11aa3bfabc7042e1761035"
@ -26402,6 +26444,11 @@ stack-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
integrity sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=
stackframe@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4"
integrity sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=
stackframe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.1.0.tgz#e3fc2eb912259479c9822f7d1f1ff365bd5cbc83"