From bd7b3576093394eeaa93a2326ecd40fdcd27bc1e Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 6 Apr 2020 10:29:14 -0500 Subject: [PATCH] [7.x] Instrument Kibana with APM RUM agent (#44281) (#62482) * 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 * Suggested fixes Co-authored-by: Vignesh Shanmugam Co-authored-by: Elastic Machine --- .gitignore | 1 - config/apm.js | 87 ------------------- package.json | 1 + src/apm.js | 78 +++++++++++++++-- src/dev/build/tasks/copy_source_task.js | 1 - src/legacy/ui/apm/index.js | 69 +++++++++++++++ src/legacy/ui/public/routes/route_manager.js | 18 +++- .../ui/ui_bundles/app_entry_template.js | 5 ++ src/legacy/ui/ui_render/ui_render_mixin.js | 6 +- .../apm/public/new-platform/plugin.tsx | 3 +- .../plugins/apm/typings/apm-rum-react.d.ts | 7 ++ x-pack/package.json | 2 +- yarn.lock | 44 +++++++++- 13 files changed, 219 insertions(+), 103 deletions(-) delete mode 100644 config/apm.js create mode 100644 src/legacy/ui/apm/index.js create mode 100644 x-pack/legacy/plugins/apm/typings/apm-rum-react.d.ts diff --git a/.gitignore b/.gitignore index fe91c32dfc92b..bd7a954f950e9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ disabledPlugins webpackstats.json /config/* !/config/kibana.yml -!/config/apm.js coverage selenium .babel_register_cache.json diff --git a/config/apm.js b/config/apm.js deleted file mode 100644 index 0cfcd759f163b..0000000000000 --- a/config/apm.js +++ /dev/null @@ -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 {}; - } -} diff --git a/package.json b/package.json index 7bc5b6bda5d93..8a4af324e8cd1 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "dependencies": { "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", + "@elastic/apm-rum": "^4.6.0", "@elastic/charts": "^18.1.1", "@elastic/datemath": "5.0.2", "@elastic/ems-client": "7.8.0", diff --git a/src/apm.js b/src/apm.js index cea6f8fc072aa..e3f4d84d9b523 100644 --- a/src/apm.js +++ b/src/apm.js @@ -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'); -module.exports = function(serviceName = name) { - if (process.env.kbnWorkerType === 'optmzr') return; +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 conf = { - serviceName: `${serviceName}-${version.replace(/\./g, '_')}`, +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); - const configFile = join(__dirname, '..', 'config', 'apm.js'); +module.exports = function(serviceName = name) { + if (process.env.kbnWorkerType === 'optmzr') return; - 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; diff --git a/src/dev/build/tasks/copy_source_task.js b/src/dev/build/tasks/copy_source_task.js index e5698c37ba16f..ee9dc159de47f 100644 --- a/src/dev/build/tasks/copy_source_task.js +++ b/src/dev/build/tasks/copy_source_task.js @@ -46,7 +46,6 @@ export const CopySourceTask = { 'typings/**', 'webpackShims/**', 'config/kibana.yml', - 'config/apm.js', 'tsconfig*.json', '.i18nrc.json', 'kibana.d.ts', diff --git a/src/legacy/ui/apm/index.js b/src/legacy/ui/apm/index.js new file mode 100644 index 0000000000000..e2ff415865b56 --- /dev/null +++ b/src/legacy/ui/apm/index.js @@ -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; +} diff --git a/src/legacy/ui/public/routes/route_manager.js b/src/legacy/ui/public/routes/route_manager.js index cf6413fb5ba7e..1cf2a5fc5a64a 100644 --- a/src/legacy/ui/public/routes/route_manager.js +++ b/src/legacy/ui/public/routes/route_manager.js @@ -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; diff --git a/src/legacy/ui/ui_bundles/app_entry_template.js b/src/legacy/ui/ui_bundles/app_entry_template.js index 966090db397f1..a1c3a153a196c 100644 --- a/src/legacy/ui/ui_bundles/app_entry_template.js +++ b/src/legacy/ui/ui_bundles/app_entry_template.js @@ -17,6 +17,8 @@ * under the License. */ +import { apmImport, apmInit } from '../apm'; + export const appEntryTemplate = bundle => ` /** * Kibana entry file @@ -26,11 +28,14 @@ export const appEntryTemplate = bundle => ` * context: ${bundle.getContext()} */ +${apmImport()} import { i18n } from '@kbn/i18n'; import { CoreSystem } from '__kibanaCore__' const injectedMetadata = JSON.parse(document.querySelector('kbn-injected-metadata').getAttribute('data')); +${apmInit('injectedMetadata.vars.apmConfig')} + i18n.load(injectedMetadata.i18n.translationsUrl) .catch(e => e) .then((i18nError) => { diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 39cf05f04a029..0912d8683fc48 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -23,6 +23,7 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; import * as UiSharedDeps from '@kbn/ui-shared-deps'; import { AppBootstrap } from './bootstrap'; +import { getApmConfig } from '../apm'; import { DllCompiler } from '../../../optimize/dynamic_dll_plugin'; /** @@ -185,7 +186,10 @@ export function uiRenderMixin(kbnServer, server, config) { uiSettings: { asScopedToClient }, } = kbnServer.newPlatform.__internals; const uiSettings = asScopedToClient(savedObjects.getClient(h.request)); - const vars = await legacy.getVars(app.getId(), h.request, overrides); + const vars = await legacy.getVars(app.getId(), h.request, { + apmConfig: getApmConfig(app), + ...overrides, + }); const content = await rendering.render(h.request, uiSettings, { app, includeUserSettings, diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index 804d127bbd75b..a291678e9a20c 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -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 { i18n } from '@kbn/i18n'; import { AlertType } from '../../../../../plugins/apm/common/alert_types'; @@ -62,7 +63,7 @@ const App = () => { {routes.map((route, i) => ( - + ))} diff --git a/x-pack/legacy/plugins/apm/typings/apm-rum-react.d.ts b/x-pack/legacy/plugins/apm/typings/apm-rum-react.d.ts new file mode 100644 index 0000000000000..6f500caabd824 --- /dev/null +++ b/x-pack/legacy/plugins/apm/typings/apm-rum-react.d.ts @@ -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'; diff --git a/x-pack/package.json b/x-pack/package.json index 3b8d5da3b9644..c63db36a8e290 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -180,8 +180,8 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@babel/runtime": "^7.9.2", + "@elastic/apm-rum-react": "^0.3.2", "@elastic/datemath": "5.0.2", - "@elastic/ems-client": "7.7.1", "@elastic/ems-client": "7.8.0", "@elastic/eui": "21.0.1", "@elastic/filesaver": "1.1.2", diff --git a/yarn.lock b/yarn.lock index dd3c0772ef3c0..cda32a30d1120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1172,6 +1172,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@^18.1.1": version "18.2.0" resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.2.0.tgz#e141151b4d7ecc71c9f6f235f8ce141665c67195" @@ -11854,6 +11879,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" @@ -11975,7 +12007,7 @@ 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.4: +es6-promise@^4.2.8, es6-promise@~4.2.4: 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== @@ -21582,6 +21614,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" @@ -27109,6 +27146,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"