diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/instruction_set.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/instruction_set.test.js.snap index a9f9823047c0b..073d20b4bf804 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/instruction_set.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/instruction_set.test.js.snap @@ -52,8 +52,10 @@ exports[`render 1`] = ` "do stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 0, "title": "step 1", @@ -65,8 +67,10 @@ exports[`render 1`] = ` "do more stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 1, "title": "step 2", @@ -129,8 +133,10 @@ exports[`statusCheckState checking status 1`] = ` "do stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 0, "title": "step 1", @@ -142,8 +148,10 @@ exports[`statusCheckState checking status 1`] = ` "do more stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 1, "title": "step 2", @@ -236,8 +244,10 @@ exports[`statusCheckState failed status check - error 1`] = ` "do stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 0, "title": "step 1", @@ -249,8 +259,10 @@ exports[`statusCheckState failed status check - error 1`] = ` "do more stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 1, "title": "step 2", @@ -347,8 +359,10 @@ exports[`statusCheckState failed status check - no data 1`] = ` "do stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 0, "title": "step 1", @@ -360,8 +374,10 @@ exports[`statusCheckState failed status check - no data 1`] = ` "do more stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 1, "title": "step 2", @@ -458,8 +474,10 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = ` "do stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 0, "title": "step 1", @@ -471,8 +489,10 @@ exports[`statusCheckState initial state - no check has been attempted 1`] = ` "do more stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 1, "title": "step 2", @@ -565,8 +585,10 @@ exports[`statusCheckState successful status check 1`] = ` "do stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 0, "title": "step 1", @@ -578,8 +600,10 @@ exports[`statusCheckState successful status check 1`] = ` "do more stuff in command line", ] } + isCloudEnabled={false} paramValues={Object {}} replaceTemplateStrings={[Function]} + variantId="OSX" />, "key": 1, "title": "step 2", diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap index f819569cd422d..ac697fae17f69 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap @@ -31,6 +31,7 @@ exports[`isCloudEnabled is false should not render instruction toggle when ON_PR }, ] } + isCloudEnabled={false} key="0" offset={1} onStatusCheck={[Function]} @@ -107,6 +108,7 @@ exports[`isCloudEnabled is false should render ON_PREM instructions with instruc }, ] } + isCloudEnabled={false} key="0" offset={1} onStatusCheck={[Function]} @@ -154,6 +156,7 @@ exports[`should render ELASTIC_CLOUD instructions when isCloudEnabled is true 1` }, ] } + isCloudEnabled={true} key="0" offset={1} onStatusCheck={[Function]} diff --git a/src/plugins/home/public/application/components/tutorial/instruction.js b/src/plugins/home/public/application/components/tutorial/instruction.js index 373f8c318a504..e4b3b3f321bf9 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction.js +++ b/src/plugins/home/public/application/components/tutorial/instruction.js @@ -18,6 +18,7 @@ import { EuiCopy, EuiButton, EuiLoadingSpinner, + EuiErrorBoundary, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -31,6 +32,8 @@ export function Instruction({ textPre, replaceTemplateStrings, customComponentName, + variantId, + isCloudEnabled, }) { const { tutorialService, http, uiSettings, getBasePath } = getServices(); @@ -96,18 +99,22 @@ export function Instruction({ {commandBlock} - {post} - {LazyCustomComponent && ( }> - + + + )} + {post} + ); @@ -120,4 +127,6 @@ Instruction.propTypes = { textPre: PropTypes.string, replaceTemplateStrings: PropTypes.func.isRequired, customComponentName: PropTypes.string, + variantId: PropTypes.string, + isCloudEnabled: PropTypes.bool.isRequired, }; diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.js b/src/plugins/home/public/application/components/tutorial/instruction_set.js index da368120d493c..08b55a527b3cf 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.js @@ -187,6 +187,8 @@ class InstructionSetUi extends React.Component { textPost={instruction.textPost} replaceTemplateStrings={this.props.replaceTemplateStrings} customComponentName={instruction.customComponentName} + variantId={instructionVariant.id} + isCloudEnabled={this.props.isCloudEnabled} /> ); return { @@ -320,6 +322,7 @@ InstructionSetUi.propTypes = { paramValues: PropTypes.object.isRequired, setParameter: PropTypes.func, replaceTemplateStrings: PropTypes.func.isRequired, + isCloudEnabled: PropTypes.bool.isRequired, }; export const InstructionSet = injectI18n(InstructionSetUi); diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.test.js b/src/plugins/home/public/application/components/tutorial/instruction_set.test.js index 539732a1c51a9..1bce4f72fde60 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.test.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.test.js @@ -49,6 +49,7 @@ test('render', () => { offset={1} paramValues={{}} replaceTemplateStrings={() => {}} + isCloudEnabled={false} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -74,6 +75,7 @@ describe('statusCheckState', () => { statusCheckConfig={statusCheckConfig} replaceTemplateStrings={() => {}} statusCheckState={StatusCheckStates.FETCHING} + isCloudEnabled={false} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -90,6 +92,7 @@ describe('statusCheckState', () => { statusCheckConfig={statusCheckConfig} replaceTemplateStrings={() => {}} statusCheckState={StatusCheckStates.FETCHING} + isCloudEnabled={false} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -106,6 +109,7 @@ describe('statusCheckState', () => { statusCheckConfig={statusCheckConfig} replaceTemplateStrings={() => {}} statusCheckState={StatusCheckStates.ERROR} + isCloudEnabled={false} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -122,6 +126,7 @@ describe('statusCheckState', () => { statusCheckConfig={statusCheckConfig} replaceTemplateStrings={() => {}} statusCheckState={StatusCheckStates.NO_DATA} + isCloudEnabled={false} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line @@ -138,6 +143,7 @@ describe('statusCheckState', () => { statusCheckConfig={statusCheckConfig} replaceTemplateStrings={() => {}} statusCheckState={StatusCheckStates.HAS_DATA} + isCloudEnabled={false} /> ); expect(component).toMatchSnapshot(); // eslint-disable-line diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 92bbb92fa0850..52daa53d4585c 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -301,6 +301,7 @@ class TutorialUi extends React.Component { setParameter={this.setParameter} replaceTemplateStrings={this.props.replaceTemplateStrings} key={index} + isCloudEnabled={this.props.isCloudEnabled} /> ); }); diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 012856ca9213c..0f0d072799061 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -175,6 +175,16 @@ export class ApmPlugin implements Plugin { () => import('./tutorial/tutorial_fleet_instructions') ); + pluginSetupDeps.home?.tutorials.registerCustomComponent( + 'TutorialConfigAgent', + () => import('./tutorial/config_agent') + ); + + pluginSetupDeps.home?.tutorials.registerCustomComponent( + 'TutorialConfigAgentRumScript', + () => import('./tutorial/config_agent/rum_script') + ); + plugins.observability.dashboard.register({ appName: 'apm', hasData: async () => { diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts new file mode 100644 index 0000000000000..97b5f3315bcdb --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/django.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const django = `# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.addAgentComment', + { + defaultMessage: 'Add the agent to the installed apps', + } +)} +INSTALLED_APPS = ( +'elasticapm.contrib.django', +# ... +) + +ELASTIC_APM = {curlyOpen} +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment', + { + defaultMessage: 'Set the required service name. Allowed characters:', + } +)} +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.allowedCharactersComment', + { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + } +)} +'SERVICE_NAME': '', + +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } +)} +'SECRET_TOKEN': '{{{secretToken}}}', + +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } +)} +'SERVER_URL': '{{{apmServerUrl}}}', + +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } +)} +'ENVIRONMENT': 'production', +{curlyClose} + +# ${i18n.translate( + 'xpack.apm.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment', + { + defaultMessage: 'To send performance metrics, add our tracing middleware:', + } +)} +MIDDLEWARE = ( +'elasticapm.contrib.django.middleware.TracingMiddleware', +#... +)`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/dotnet.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/dotnet.ts new file mode 100644 index 0000000000000..e083a2b45c716 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/dotnet.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export const dotnet = `{ +"ElasticApm": { +"SecretToken": "{{{secretToken}}}", +"ServerUrls": "{{{apmServerUrl}}}", //Set custom APM Server URL (default: http://localhost:8200) +"ServiceName": "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application +"Environment": "production", // Set the service environment +} +}`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts new file mode 100644 index 0000000000000..e4d7fd188e7c6 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/flask.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const flask = `# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment', + { + defaultMessage: 'initialize using environment variables', + } +)} +from elasticapm.contrib.flask import ElasticAPM +app = Flask(__name__) +apm = ElasticAPM(app) + +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.configureElasticApmComment', + { + defaultMessage: + "or configure to use ELASTIC_APM in your application's settings", + } +)} +from elasticapm.contrib.flask import ElasticAPM +app.config['ELASTIC_APM'] = {curlyOpen} +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment', + { + defaultMessage: 'Set the required service name. Allowed characters:', + } +)} +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.allowedCharactersComment', + { + defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', + } +)} +'SERVICE_NAME': '', + +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } +)} +'SECRET_TOKEN': '{{{secretToken}}}', + +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } +)} +'SERVER_URL': '{{{apmServerUrl}}}', + +# ${i18n.translate( + 'xpack.apm.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } +)} +'ENVIRONMENT': 'production', +{curlyClose} + +apm = ElasticAPM(app)`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts new file mode 100644 index 0000000000000..5dc66e2230524 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.test.ts @@ -0,0 +1,536 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getCommands } from './get_commands'; + +describe('getCommands', () => { + describe('unknown agent', () => { + it('renders empty command', () => { + const commands = getCommands({ + variantId: 'foo', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).toBe(''); + }); + }); + describe('java agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'java', + policyDetails: {}, + }); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls= \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'java', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=localhost:8220 \\\\ + -Delastic.apm.secret_token=foobar \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + }); + describe('RUM(js) agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'js', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "import { init as initApm } from '@elastic/apm-rum' + var apm = initApm({ + + // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + serviceName: 'your-app-name', + + // Set custom APM Server URL (default: http://localhost:8200) + serverUrl: '', + + // Set the service version (required for source map feature) + serviceVersion: '', + + // Set the service environment + environment: 'production' + })" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'js', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "import { init as initApm } from '@elastic/apm-rum' + var apm = initApm({ + + // Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space) + serviceName: 'your-app-name', + + // Set custom APM Server URL (default: http://localhost:8200) + serverUrl: 'localhost:8220', + + // Set the service version (required for source map feature) + serviceVersion: '', + + // Set the service environment + environment: 'production' + })" + `); + }); + }); + describe('Node.js agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'node', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "// Add this to the VERY top of the first file loaded in your app + var apm = require('elastic-apm-node').start({ + + // Override the service name from package.json + // Allowed characters: a-z, A-Z, 0-9, -, _, and space + serviceName: '', + + // Use if APM Server requires a secret token + secretToken: '', + + // Set the custom APM Server URL (default: http://localhost:8200) + serverUrl: '', + + // Set the service environment + environment: 'production' + })" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'node', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "// Add this to the VERY top of the first file loaded in your app + var apm = require('elastic-apm-node').start({ + + // Override the service name from package.json + // Allowed characters: a-z, A-Z, 0-9, -, _, and space + serviceName: '', + + // Use if APM Server requires a secret token + secretToken: 'foobar', + + // Set the custom APM Server URL (default: http://localhost:8200) + serverUrl: 'localhost:8220', + + // Set the service environment + environment: 'production' + })" + `); + }); + }); + describe('Django agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'django', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Add the agent to the installed apps + INSTALLED_APPS = ( + 'elasticapm.contrib.django', + # ... + ) + + ELASTIC_APM = {curlyOpen} + # Set the required service name. Allowed characters: + # a-z, A-Z, 0-9, -, _, and space + 'SERVICE_NAME': '', + + # Use if APM Server requires a secret token + 'SECRET_TOKEN': '', + + # Set the custom APM Server URL (default: http://localhost:8200) + 'SERVER_URL': '', + + # Set the service environment + 'ENVIRONMENT': 'production', + {curlyClose} + + # To send performance metrics, add our tracing middleware: + MIDDLEWARE = ( + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... + )" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'django', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Add the agent to the installed apps + INSTALLED_APPS = ( + 'elasticapm.contrib.django', + # ... + ) + + ELASTIC_APM = {curlyOpen} + # Set the required service name. Allowed characters: + # a-z, A-Z, 0-9, -, _, and space + 'SERVICE_NAME': '', + + # Use if APM Server requires a secret token + 'SECRET_TOKEN': 'foobar', + + # Set the custom APM Server URL (default: http://localhost:8200) + 'SERVER_URL': 'localhost:8220', + + # Set the service environment + 'ENVIRONMENT': 'production', + {curlyClose} + + # To send performance metrics, add our tracing middleware: + MIDDLEWARE = ( + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... + )" + `); + }); + }); + describe('Flask agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'flask', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# initialize using environment variables + from elasticapm.contrib.flask import ElasticAPM + app = Flask(__name__) + apm = ElasticAPM(app) + + # or configure to use ELASTIC_APM in your application's settings + from elasticapm.contrib.flask import ElasticAPM + app.config['ELASTIC_APM'] = {curlyOpen} + # Set the required service name. Allowed characters: + # a-z, A-Z, 0-9, -, _, and space + 'SERVICE_NAME': '', + + # Use if APM Server requires a secret token + 'SECRET_TOKEN': '', + + # Set the custom APM Server URL (default: http://localhost:8200) + 'SERVER_URL': '', + + # Set the service environment + 'ENVIRONMENT': 'production', + {curlyClose} + + apm = ElasticAPM(app)" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'flask', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# initialize using environment variables + from elasticapm.contrib.flask import ElasticAPM + app = Flask(__name__) + apm = ElasticAPM(app) + + # or configure to use ELASTIC_APM in your application's settings + from elasticapm.contrib.flask import ElasticAPM + app.config['ELASTIC_APM'] = {curlyOpen} + # Set the required service name. Allowed characters: + # a-z, A-Z, 0-9, -, _, and space + 'SERVICE_NAME': '', + + # Use if APM Server requires a secret token + 'SECRET_TOKEN': 'foobar', + + # Set the custom APM Server URL (default: http://localhost:8200) + 'SERVER_URL': 'localhost:8220', + + # Set the service environment + 'ENVIRONMENT': 'production', + {curlyClose} + + apm = ElasticAPM(app)" + `); + }); + }); + describe('Ruby on Rails agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'rails', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space + # Defaults to the name of your Rails app + service_name: 'my-service' + + # Use if APM Server requires a secret token + secret_token: '' + + # Set the custom APM Server URL (default: http://localhost:8200) + server_url: '' + + # Set the service environment + environment: 'production'" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'rails', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space + # Defaults to the name of your Rails app + service_name: 'my-service' + + # Use if APM Server requires a secret token + secret_token: 'foobar' + + # Set the custom APM Server URL (default: http://localhost:8200) + server_url: 'localhost:8220' + + # Set the service environment + environment: 'production'" + `); + }); + }); + describe('Rack agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'rack', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space + # Defaults to the name of your Rack app's class. + service_name: 'my-service' + + # Use if APM Server requires a token + secret_token: '' + + # Set custom APM Server URL (default: http://localhost:8200) + server_url: '', + + # Set the service environment + environment: 'production'" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'rack', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space + # Defaults to the name of your Rack app's class. + service_name: 'my-service' + + # Use if APM Server requires a token + secret_token: 'foobar' + + # Set custom APM Server URL (default: http://localhost:8200) + server_url: 'localhost:8220', + + # Set the service environment + environment: 'production'" + `); + }); + }); + describe('Go agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'go', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables: + + # Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space. + # If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used. + export ELASTIC_APM_SERVICE_NAME= + + # Set custom APM Server URL (default: http://localhost:8200) + export ELASTIC_APM_SERVER_URL= + + # Use if APM Server requires a secret token + export ELASTIC_APM_SECRET_TOKEN= + + # Set the service environment + export ELASTIC_APM_ENVIRONMENT= + " + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'go', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables: + + # Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space. + # If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used. + export ELASTIC_APM_SERVICE_NAME= + + # Set custom APM Server URL (default: http://localhost:8200) + export ELASTIC_APM_SERVER_URL=localhost:8220 + + # Use if APM Server requires a secret token + export ELASTIC_APM_SECRET_TOKEN=foobar + + # Set the service environment + export ELASTIC_APM_ENVIRONMENT= + " + `); + }); + }); + describe('dotNet agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'dotnet', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "{ + \\"ElasticApm\\": { + \\"SecretToken\\": \\"\\", + \\"ServerUrls\\": \\"\\", //Set custom APM Server URL (default: http://localhost:8200) + \\"ServiceName\\": \\"MyApp\\", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application + \\"Environment\\": \\"production\\", // Set the service environment + } + }" + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'dotnet', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "{ + \\"ElasticApm\\": { + \\"SecretToken\\": \\"foobar\\", + \\"ServerUrls\\": \\"localhost:8220\\", //Set custom APM Server URL (default: http://localhost:8200) + \\"ServiceName\\": \\"MyApp\\", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application + \\"Environment\\": \\"production\\", // Set the service environment + } + }" + `); + }); + }); + describe('PHP agent', () => { + it('renders empty commands', () => { + const commands = getCommands({ + variantId: 'php', + policyDetails: {}, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "elastic_apm.server_url=\\"\\" + elastic.apm.secret_token=\\"\\" + elastic_apm.service_name=\\"My service\\" + " + `); + }); + it('renders with secret token and url', () => { + const commands = getCommands({ + variantId: 'php', + policyDetails: { + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }, + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "elastic_apm.server_url=\\"localhost:8220\\" + elastic.apm.secret_token=\\"foobar\\" + elastic_apm.service_name=\\"My service\\" + " + `); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.ts new file mode 100644 index 0000000000000..73a388c3f735e --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/get_commands.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import Mustache from 'mustache'; +import { java } from './java'; +import { node } from './node'; +import { django } from './django'; +import { flask } from './flask'; +import { rails } from './rails'; +import { rack } from './rack'; +import { go } from './go'; +import { dotnet } from './dotnet'; +import { php } from './php'; +import { rum, rumScript } from './rum'; + +const commandsMap: Record = { + java, + node, + django, + flask, + rails, + rack, + go, + dotnet, + php, + js: rum, + js_script: rumScript, +}; + +export function getCommands({ + variantId, + policyDetails, +}: { + variantId: string; + policyDetails: { + apmServerUrl?: string; + secretToken?: string; + }; +}) { + const commands = commandsMap[variantId]; + if (!commands) { + return ''; + } + return Mustache.render(commands, policyDetails); +} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/go.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/go.ts new file mode 100644 index 0000000000000..a3900420d6fde --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/go.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const go = `# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', + { + defaultMessage: 'Initialize using environment variables:', + } +)} + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.setServiceNameComment', + { + defaultMessage: + 'Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space.', + } +)} +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.usedExecutableNameComment', + { + defaultMessage: + 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', + } +)} +export ELASTIC_APM_SERVICE_NAME= + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } +)} +export ELASTIC_APM_SERVER_URL={{{apmServerUrl}}} + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } +)} +export ELASTIC_APM_SECRET_TOKEN={{{secretToken}}} + +# ${i18n.translate( + 'xpack.apm.tutorial.goClient.configure.commands.setServiceEnvironment', + { + defaultMessage: 'Set the service environment', + } +)} +export ELASTIC_APM_ENVIRONMENT= +`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/java.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/java.ts new file mode 100644 index 0000000000000..249907a9b0c4b --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/java.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const java = `java -javaagent:/path/to/elastic-apm-agent-.jar \\ +-Delastic.apm.service_name=my-application \\ +-Delastic.apm.server_urls={{{apmServerUrl}}} \\ +-Delastic.apm.secret_token={{{secretToken}}} \\ +-Delastic.apm.environment=production \\ +-Delastic.apm.application_packages=org.example \\ +-jar my-application.jar`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/node.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/node.ts new file mode 100644 index 0000000000000..31f9fac0ed480 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/node.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; + +export const node = `// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.addThisToTheFileTopComment', + { + defaultMessage: + 'Add this to the VERY top of the first file loaded in your app', + } +)} +var apm = require('elastic-apm-node').start({ + +// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment', + { + defaultMessage: 'Override the service name from package.json', + } +)} +// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.allowedCharactersComment', + { + defaultMessage: 'Allowed characters: a-z, A-Z, 0-9, -, _, and space', + } +)} +serviceName: '', + +// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a secret token', + } +)} +secretToken: '{{{secretToken}}}', + +// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } +)} +serverUrl: '{{{apmServerUrl}}}', + +// ${i18n.translate( + 'xpack.apm.tutorial.nodeClient.configure.commands.setCustomServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } +)} +environment: 'production' +})`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts new file mode 100644 index 0000000000000..ea7e8764f89ad --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/php.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const php = `elastic_apm.server_url="{{{apmServerUrl}}}" +elastic.apm.secret_token="{{{secretToken}}}" +elastic_apm.service_name="My service" +`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/rack.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/rack.ts new file mode 100644 index 0000000000000..9195ad9f15666 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/rack.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const rack = `# config/elastic_apm.yml: + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.setServiceNameComment', + { + defaultMessage: + 'Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', + } +)} +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', + { + defaultMessage: "Defaults to the name of your Rack app's class.", + } +)} +service_name: 'my-service' + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', + { + defaultMessage: 'Use if APM Server requires a token', + } +)} +secret_token: '{{{secretToken}}}' + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.setCustomApmServerComment', + { + defaultMessage: 'Set custom APM Server URL (default: {defaultServerUrl})', + values: { defaultServerUrl: 'http://localhost:8200' }, + } +)} +server_url: '{{{apmServerUrl}}}', + +# ${i18n.translate( + 'xpack.apm.tutorial.rackClient.createConfig.commands.setServiceEnvironment', + { + defaultMessage: 'Set the service environment', + } +)} +environment: 'production'`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/rails.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/rails.ts new file mode 100644 index 0000000000000..0f8a5508e1ceb --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/rails.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const rails = `# config/elastic_apm.yml: + +# Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space +# Defaults to the name of your Rails app +service_name: 'my-service' + +# Use if APM Server requires a secret token +secret_token: '{{{secretToken}}}' + +# Set the custom APM Server URL (default: http://localhost:8200) +server_url: '{{{apmServerUrl}}}' + +# Set the service environment +environment: 'production'`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/commands/rum.ts b/x-pack/plugins/apm/public/tutorial/config_agent/commands/rum.ts new file mode 100644 index 0000000000000..f5de61f64c63a --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/commands/rum.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const rum = `import { init as initApm } from '@elastic/apm-rum' +var apm = initApm({ + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment', + { + defaultMessage: + 'Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)', + } + )} + serviceName: 'your-app-name', + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment', + { + defaultMessage: + 'Set custom APM Server URL (default: {defaultApmServerUrl})', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } + )} + serverUrl: '{{{apmServerUrl}}}', + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setServiceVersionComment', + { + defaultMessage: + 'Set the service version (required for source map feature)', + } + )} + serviceVersion: '', + + // ${i18n.translate( + 'xpack.apm.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment', + { + defaultMessage: 'Set the service environment', + } + )} + environment: 'production' +})`; + +export const rumScript = `\ + + +`; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx new file mode 100644 index 0000000000000..33f171ab88247 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/config_agent.stories.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import { HttpStart } from 'kibana/public'; +import React from 'react'; +import TutorialConfigAgent from './'; +import { APIReturnType } from '../..//services/rest/createCallApmApi'; + +export type APIResponseType = APIReturnType<'GET /api/apm/fleet/agents'>; + +interface Args { + apmAgent: string; + onPrem: boolean; + hasFleetPoliciesWithApmIntegration: boolean; + hasCloudPolicyWithApmIntegration: boolean; +} + +const policyElasticAgentOnCloudAgent: APIResponseType['fleetAgents'][0] = { + id: 'policy-elastic-agent-on-cloud', + name: 'Elastic Cloud agent policy', + apmServerUrl: 'apm_cloud_url', + secretToken: 'apm_cloud_token', +}; + +const fleetAgents: APIResponseType['fleetAgents'] = [ + { + id: '1', + name: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + }, + { + id: '2', + name: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + }, +]; + +function Wrapper({ + hasFleetPoliciesWithApmIntegration, + apmAgent, + onPrem, + hasCloudPolicyWithApmIntegration, +}: Args) { + const http = ({ + get: () => ({ + fleetAgents: [ + ...(hasFleetPoliciesWithApmIntegration ? fleetAgents : []), + ...(hasCloudPolicyWithApmIntegration + ? [policyElasticAgentOnCloudAgent] + : []), + ], + cloudStandaloneSetup: { + apmServerUrl: 'cloud_url', + secretToken: 'foo', + }, + }), + } as unknown) as HttpStart; + return ( + + ); +} +export const Integration: Story = (args) => { + return ; +}; + +Integration.args = { + apmAgent: 'java', + onPrem: true, + hasFleetPoliciesWithApmIntegration: false, + hasCloudPolicyWithApmIntegration: false, +}; + +export default { + title: 'app/Tutorial/AgentConfig', + component: TutorialConfigAgent, + argTypes: { + apmAgent: { + control: { + type: 'select', + options: [ + 'java', + 'node', + 'django', + 'flask', + 'rails', + 'rack', + 'go', + 'dotnet', + 'php', + 'js', + 'js_script', + ], + }, + }, + onPrem: { + control: { type: 'boolean', options: [true, false] }, + }, + hasFleetPoliciesWithApmIntegration: { + control: { type: 'boolean', options: [true, false] }, + }, + hasCloudPolicyWithApmIntegration: { + control: { type: 'boolean', options: [true, false] }, + }, + }, +}; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx new file mode 100644 index 0000000000000..c5261cfc1dc04 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButton, EuiCopy } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface Props { + commands: string; +} +export function CopyCommands({ commands }: Props) { + return ( + + {(copy) => ( + + {i18n.translate('xpack.apm.tutorial.copySnippet', { + defaultMessage: 'Copy snippet', + })} + + )} + + ); +} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts new file mode 100644 index 0000000000000..90c9aab80f6f5 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.test.ts @@ -0,0 +1,318 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getPolicyOptions } from './get_policy_options'; +import { APIReturnType } from '../../services/rest/createCallApmApi'; + +type APIResponseType = APIReturnType<'GET /api/apm/fleet/agents'>; + +const policyElasticAgentOnCloudAgent = { + id: 'policy-elastic-agent-on-cloud', + name: 'Elastic Cloud agent policy', + apmServerUrl: 'apm_cloud_url', + secretToken: 'apm_cloud_token', +}; + +const fleetAgents = [ + { + id: '1', + name: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + }, + { + id: '2', + name: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + }, +]; + +describe('getPolicyOptions', () => { + describe('running on cloud', () => { + describe('with APM on cloud', () => { + it('shows apm on cloud standalone option', () => { + const data: APIResponseType = { + fleetAgents: [], + cloudStandaloneSetup: { + apmServerUrl: 'cloud_url', + secretToken: 'cloud_token', + }, + }; + const options = getPolicyOptions({ + isCloudEnabled: true, + data, + }); + expect(options).toEqual([ + { + key: 'cloud', + type: 'standalone', + label: 'Default Standalone configuration', + apmServerUrl: 'cloud_url', + secretToken: 'cloud_token', + isVisible: true, + isSelected: true, + }, + ]); + }); + it('shows apm on cloud standalone option and fleet agents options', () => { + const data: APIResponseType = { + fleetAgents, + cloudStandaloneSetup: { + apmServerUrl: 'cloud_url', + secretToken: 'cloud_token', + }, + }; + const options = getPolicyOptions({ + isCloudEnabled: true, + data, + }); + + expect(options).toEqual([ + { + key: 'cloud', + type: 'standalone', + label: 'Default Standalone configuration', + apmServerUrl: 'cloud_url', + secretToken: 'cloud_token', + isVisible: true, + isSelected: true, + }, + + { + key: '1', + type: 'fleetAgents', + label: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + isVisible: true, + isSelected: false, + }, + { + key: '2', + type: 'fleetAgents', + label: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + isVisible: true, + isSelected: false, + }, + ]); + }); + it('selects policy elastic agent on cloud when available', () => { + const data: APIResponseType = { + fleetAgents: [policyElasticAgentOnCloudAgent, ...fleetAgents], + cloudStandaloneSetup: { + apmServerUrl: 'cloud_url', + secretToken: 'cloud_token', + }, + }; + const options = getPolicyOptions({ + isCloudEnabled: true, + data, + }); + + expect(options).toEqual([ + { + key: 'policy-elastic-agent-on-cloud', + type: 'fleetAgents', + label: 'Elastic Cloud agent policy', + apmServerUrl: 'apm_cloud_url', + secretToken: 'apm_cloud_token', + isVisible: true, + isSelected: true, + }, + { + key: '1', + type: 'fleetAgents', + label: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + isVisible: true, + isSelected: false, + }, + { + key: '2', + type: 'fleetAgents', + label: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + isVisible: true, + isSelected: false, + }, + ]); + }); + }); + describe('with APM on prem', () => { + it('shows apm on prem standalone option', () => { + const data: APIResponseType = { + fleetAgents: [], + cloudStandaloneSetup: undefined, + }; + const options = getPolicyOptions({ + isCloudEnabled: true, + data, + }); + + expect(options).toEqual([ + { + key: 'onPrem', + type: 'standalone', + label: 'Default Standalone configuration', + apmServerUrl: 'http://localhost:8200', + secretToken: '', + isVisible: true, + isSelected: true, + }, + ]); + }); + it('shows apm on prem standalone option and fleet agents options', () => { + const data: APIResponseType = { + fleetAgents, + cloudStandaloneSetup: undefined, + }; + const options = getPolicyOptions({ + isCloudEnabled: true, + data, + }); + expect(options).toEqual([ + { + key: 'onPrem', + type: 'standalone', + label: 'Default Standalone configuration', + apmServerUrl: 'http://localhost:8200', + secretToken: '', + isVisible: true, + isSelected: true, + }, + + { + key: '1', + type: 'fleetAgents', + label: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + isVisible: true, + isSelected: false, + }, + { + key: '2', + type: 'fleetAgents', + label: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + isVisible: true, + isSelected: false, + }, + ]); + }); + it('selects policy elastic agent on cloud when available', () => { + const data: APIResponseType = { + fleetAgents: [policyElasticAgentOnCloudAgent, ...fleetAgents], + cloudStandaloneSetup: undefined, + }; + const options = getPolicyOptions({ + isCloudEnabled: true, + data, + }); + + expect(options).toEqual([ + { + key: 'policy-elastic-agent-on-cloud', + type: 'fleetAgents', + label: 'Elastic Cloud agent policy', + apmServerUrl: 'apm_cloud_url', + secretToken: 'apm_cloud_token', + isVisible: true, + isSelected: true, + }, + { + key: '1', + type: 'fleetAgents', + label: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + isVisible: true, + isSelected: false, + }, + { + key: '2', + type: 'fleetAgents', + label: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + isVisible: true, + isSelected: false, + }, + ]); + }); + }); + }); + describe('Running on prem', () => { + it('shows apm on prem standalone option', () => { + const data: APIResponseType = { + fleetAgents: [], + cloudStandaloneSetup: undefined, + }; + const options = getPolicyOptions({ + isCloudEnabled: false, + data, + }); + + expect(options).toEqual([ + { + key: 'onPrem', + type: 'standalone', + label: 'Default Standalone configuration', + apmServerUrl: 'http://localhost:8200', + secretToken: '', + isVisible: true, + isSelected: true, + }, + ]); + }); + it('shows apm on prem standalone option and fleet agents options', () => { + const data: APIResponseType = { + fleetAgents, + cloudStandaloneSetup: undefined, + }; + const options = getPolicyOptions({ + isCloudEnabled: false, + data, + }); + + expect(options).toEqual([ + { + key: 'onPrem', + type: 'standalone', + label: 'Default Standalone configuration', + apmServerUrl: 'http://localhost:8200', + secretToken: '', + isVisible: true, + isSelected: true, + }, + { + key: '1', + type: 'fleetAgents', + label: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + isVisible: true, + isSelected: false, + }, + { + key: '2', + type: 'fleetAgents', + label: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + isVisible: true, + isSelected: false, + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.ts b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.ts new file mode 100644 index 0000000000000..afbdc867d3e0a --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/get_policy_options.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { APIResponseType } from './'; + +const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud'; + +const DEFAULT_STANDALONE_CONFIG_LABEL = i18n.translate( + 'xpack.apm.tutorial.agent_config.defaultStandaloneConfig', + { defaultMessage: 'Default Standalone configuration' } +); + +export type PolicyOption = ReturnType[0]; + +export function getPolicyOptions({ + isCloudEnabled, + data, +}: { + isCloudEnabled: boolean; + data: APIResponseType; +}) { + const isCloudVisible = !!( + isCloudEnabled && data.cloudStandaloneSetup?.apmServerUrl + ); + + const fleetAgentsOptions = data.fleetAgents.map((agent) => { + return { + key: agent.id, + type: 'fleetAgents', + label: agent.name, + apmServerUrl: agent.apmServerUrl, + secretToken: agent.secretToken, + isVisible: true, + isSelected: agent.id === POLICY_ELASTIC_AGENT_ON_CLOUD, + }; + }); + + const hasFleetAgentsSelected = fleetAgentsOptions.some( + ({ isSelected }) => isSelected + ); + + return [ + { + key: 'cloud', + type: 'standalone', + label: DEFAULT_STANDALONE_CONFIG_LABEL, + apmServerUrl: data.cloudStandaloneSetup?.apmServerUrl, + secretToken: data.cloudStandaloneSetup?.secretToken, + isVisible: isCloudVisible && !hasFleetAgentsSelected, + isSelected: !hasFleetAgentsSelected, + }, + { + key: 'onPrem', + type: 'standalone', + label: DEFAULT_STANDALONE_CONFIG_LABEL, + apmServerUrl: 'http://localhost:8200', + secretToken: '', + isVisible: !isCloudVisible && !hasFleetAgentsSelected, + isSelected: !hasFleetAgentsSelected, + }, + ...fleetAgentsOptions, + ].filter(({ isVisible }) => isVisible); +} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx new file mode 100644 index 0000000000000..8f8afe58506a6 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.test.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { fireEvent, render, screen } from '@testing-library/react'; +import { HttpStart } from 'kibana/public'; +import React from 'react'; +import TutorialConfigAgent from './'; + +const policyElasticAgentOnCloudAgent = { + id: 'policy-elastic-agent-on-cloud', + name: 'Elastic Cloud agent policy', + apmServerUrl: 'apm_cloud_url', + secretToken: 'apm_cloud_token', +}; + +const fleetAgents = [ + { + id: '1', + name: 'agent foo', + apmServerUrl: 'foo', + secretToken: 'foo', + }, + { + id: '2', + name: 'agent bar', + apmServerUrl: 'bar', + secretToken: 'bar', + }, +]; + +describe('TutorialConfigAgent', () => { + it('renders loading component while API is being called', () => { + const component = render( + + ); + expect(component.getByTestId('loading')).toBeInTheDocument(); + }); + it('updates commands when a different policy is selected', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + let commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + + fireEvent.click(component.getByTestId('comboBoxToggleListButton')); + fireEvent.click(component.getByText('agent foo')); + commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=foo \\\\ + -Delastic.apm.secret_token=foo \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + describe('running on prem', () => { + it('selects defaul standalone by defauls', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_onPrem') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=http://localhost:8200 \\\\ + -Delastic.apm.secret_token= \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + }); + describe('running on cloud', () => { + it('selects defaul standalone by defauls', async () => { + const component = render( + + ); + expect( + await screen.findByText('Default Standalone configuration') + ).toBeInTheDocument(); + expect(component.getByTestId('policySelector_cloud')).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=cloud_url \\\\ + -Delastic.apm.secret_token=cloud_token \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + it('selects policy elastic agent on cloud when available by default', async () => { + const component = render( + + ); + expect( + await screen.findByText('Elastic Cloud agent policy') + ).toBeInTheDocument(); + expect( + component.getByTestId('policySelector_policy-elastic-agent-on-cloud') + ).toBeInTheDocument(); + const commands = component.getByTestId('commands').innerHTML; + expect(commands).not.toEqual(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-<version>.jar \\\\ + -Delastic.apm.service_name=my-application \\\\ + -Delastic.apm.server_urls=apm_cloud_url \\\\ + -Delastic.apm.secret_token=apm_cloud_token \\\\ + -Delastic.apm.environment=production \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-application.jar" + `); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx new file mode 100644 index 0000000000000..755c3eca55868 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { HttpStart } from 'kibana/public'; +import React, { useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; +import { APIReturnType } from '../..//services/rest/createCallApmApi'; +import { getCommands } from './commands/get_commands'; +import { CopyCommands } from './copy_commands'; +import { getPolicyOptions, PolicyOption } from './get_policy_options'; +import { PolicySelector } from './policy_selector'; + +export type APIResponseType = APIReturnType<'GET /api/apm/fleet/agents'>; + +const CentralizedContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +const MANAGE_FLEET_POLICIES_LABEL = i18n.translate( + 'xpack.apm.tutorial.agent_config.manageFleetPolicies', + { defaultMessage: 'Manage fleet policies' } +); + +const GET_STARTED_WITH_FLEET_LABEL = i18n.translate( + 'xpack.apm.tutorial.agent_config.getStartedWithFleet', + { defaultMessage: 'Get started with fleet' } +); + +interface Props { + variantId: string; + http: HttpStart; + basePath: string; + isCloudEnabled: boolean; +} + +function TutorialConfigAgent({ + variantId, + http, + basePath, + isCloudEnabled, +}: Props) { + const [data, setData] = useState({ + fleetAgents: [], + cloudStandaloneSetup: undefined, + }); + const [isLoading, setIsLoading] = useState(true); + const [selectedOption, setSelectedOption] = useState(); + + useEffect(() => { + async function fetchData() { + setIsLoading(true); + try { + const response = await http.get('/api/apm/fleet/agents'); + if (response) { + setData(response as APIResponseType); + } + } catch (e) { + console.error('Error while fetching fleet agents.', e); + } + } + fetchData(); + }, [http]); + + // Depending the environment running (onPrem/Cloud) different values must be available and automatically selected + const options = useMemo(() => { + const availableOptions = getPolicyOptions({ + isCloudEnabled, + data, + }); + const defaultSelectedOption = availableOptions.find( + ({ isSelected }) => isSelected + ); + setSelectedOption(defaultSelectedOption); + setIsLoading(false); + return availableOptions; + }, [data, isCloudEnabled]); + + if (isLoading) { + return ( + + + + ); + } + + const commands = getCommands({ + variantId, + policyDetails: { + apmServerUrl: selectedOption?.apmServerUrl, + secretToken: selectedOption?.secretToken, + }, + }); + + const hasFleetAgents = !!data.fleetAgents.length; + const fleetLink = hasFleetAgents + ? { + label: MANAGE_FLEET_POLICIES_LABEL, + href: `${basePath}/app/fleet#/policies`, + } + : { + label: GET_STARTED_WITH_FLEET_LABEL, + href: `${basePath}/app/integrations#/detail/apm-0.3.0/overview`, + }; + + return ( + <> + + + + setSelectedOption(newSelectedOption) + } + fleetLink={fleetLink} + /> + + + + + + + + {commands} + + + ); +} + +// eslint-disable-next-line import/no-default-export +export default TutorialConfigAgent; diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx new file mode 100644 index 0000000000000..3a0c6d70db82b --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/policy_selector.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiComboBox, + EuiComboBoxOptionOption, + EuiFormRow, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { groupBy } from 'lodash'; +import React from 'react'; +import { PolicyOption } from './get_policy_options'; + +interface Props { + options: PolicyOption[]; + selectedOption?: PolicyOption; + onChange: (selectedOption?: PolicyOption) => void; + fleetLink: { + label: string; + href: string; + }; +} + +export function PolicySelector({ + options, + selectedOption, + onChange, + fleetLink, +}: Props) { + const { fleetAgents, standalone } = groupBy(options, 'type'); + + const standaloneComboboxOptions: EuiComboBoxOptionOption[] = + standalone?.map(({ key, label }) => ({ key, label })) || []; + + const fleetAgentsComboboxOptions = fleetAgents?.length + ? [ + { + key: 'fleet_policies', + label: i18n.translate( + 'xpack.apm.tutorial.agent_config.fleetPoliciesLabel', + { defaultMessage: 'Fleet policies' } + ), + options: fleetAgents.map(({ key, label }) => ({ key, label })), + }, + ] + : []; + + return ( + + {fleetLink.label} + + } + helpText={i18n.translate( + 'xpack.apm.tutorial.agent_config.choosePolicy.helper', + { + defaultMessage: + 'Adds the selected policy configuration to the snippet below.', + } + )} + > + { + const newSelectedOption = options.find( + ({ key }) => key === selectedOptions[0].key + ); + onChange(newSelectedOption); + }} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/rum_script.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/rum_script.tsx new file mode 100644 index 0000000000000..ae3ec803ac588 --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/config_agent/rum_script.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpStart } from 'kibana/public'; +import React from 'react'; +import TutorialConfigAgent from './'; + +interface Props { + http: HttpStart; + basePath: string; + isCloudEnabled: boolean; +} + +function TutorialConfigAgentRumScript({ + http, + basePath, + isCloudEnabled, +}: Props) { + return ( + + ); +} + +// eslint-disable-next-line import/no-default-export +export default TutorialConfigAgentRumScript; diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/tutorial_fleet_instructions.stories.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/tutorial_fleet_instructions.stories.tsx new file mode 100644 index 0000000000000..40b72f06654ff --- /dev/null +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/tutorial_fleet_instructions.stories.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Story } from '@storybook/react'; +import React from 'react'; +import { HttpStart } from 'kibana/public'; +import TutorialFleetInstructions from '.'; + +interface Args { + hasFleetPoliciesWithApmIntegration: boolean; +} + +function Wrapper({ hasFleetPoliciesWithApmIntegration }: Args) { + const http = ({ + get: () => ({ hasData: hasFleetPoliciesWithApmIntegration }), + } as unknown) as HttpStart; + return ( + + ); +} + +export default { + title: 'app/Tutorial/FleetInstructions', + component: TutorialFleetInstructions, + argTypes: { + hasFleetPoliciesWithApmIntegration: { + control: { type: 'boolean', options: [true, false] }, + }, + }, +}; + +export const Instructions: Story = (args) => { + return ; +}; + +Instructions.args = { + hasFleetPoliciesWithApmIntegration: true, +}; diff --git a/x-pack/plugins/apm/server/lib/fleet/get_agents.ts b/x-pack/plugins/apm/server/lib/fleet/get_agents.ts new file mode 100644 index 0000000000000..5ee44bb3ad174 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/fleet/get_agents.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CoreSetup, + CoreStart, + SavedObjectsClientContract, +} from 'kibana/server'; +import { APMPluginStartDependencies } from '../../types'; +import { getInternalSavedObjectsClient } from '../helpers/get_internal_saved_objects_client'; + +export async function getFleetAgents({ + policyIds, + core, + fleetPluginStart, +}: { + policyIds: string[]; + core: { setup: CoreSetup; start: () => Promise }; + fleetPluginStart: NonNullable; +}) { + // @ts-ignore + const savedObjectsClient: SavedObjectsClientContract = await getInternalSavedObjectsClient( + core.setup + ); + + return await fleetPluginStart.agentPolicyService.getByIds( + savedObjectsClient, + policyIds + ); +} diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index 74ca8dc368dad..01323add276df 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -5,23 +5,28 @@ * 2.0. */ +import { keyBy } from 'lodash'; import Boom from '@hapi/boom'; import { i18n } from '@kbn/i18n'; +import { getFleetAgents } from '../lib/fleet/get_agents'; import { getApmPackgePolicies } from '../lib/fleet/get_apm_package_policies'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +const FLEET_REQUIRED_MESSAGE = i18n.translate( + 'xpack.apm.fleet_has_data.fleetRequired', + { + defaultMessage: `Fleet plugin is required`, + } +); + const hasFleetDataRoute = createApmServerRoute({ endpoint: 'GET /api/apm/fleet/has_data', options: { tags: [] }, handler: async ({ core, plugins }) => { const fleetPluginStart = await plugins.fleet?.start(); if (!fleetPluginStart) { - throw Boom.internal( - i18n.translate('xpack.apm.fleet_has_data.fleetRequired', { - defaultMessage: `Fleet plugin is required`, - }) - ); + throw Boom.internal(FLEET_REQUIRED_MESSAGE); } const packagePolicies = await getApmPackgePolicies({ core, @@ -31,6 +36,54 @@ const hasFleetDataRoute = createApmServerRoute({ }, }); -export const ApmFleetRouteRepository = createApmServerRouteRepository().add( - hasFleetDataRoute -); +const fleetAgentsRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/fleet/agents', + options: { tags: [] }, + handler: async ({ core, plugins }) => { + const cloudSetup = plugins.cloud?.setup; + const cloudStandaloneSetup = cloudSetup + ? { + apmServerUrl: cloudSetup?.apm.url, + secretToken: cloudSetup?.apm.secretToken, + } + : undefined; + + const fleetPluginStart = await plugins.fleet?.start(); + if (!fleetPluginStart) { + throw Boom.internal(FLEET_REQUIRED_MESSAGE); + } + // fetches package policies that contains APM integrations + const packagePolicies = await getApmPackgePolicies({ + core, + fleetPluginStart, + }); + + const policiesGroupedById = keyBy(packagePolicies.items, 'policy_id'); + + // fetches all agents with the found package policies + const fleetAgents = await getFleetAgents({ + policyIds: Object.keys(policiesGroupedById), + core, + fleetPluginStart, + }); + + return { + cloudStandaloneSetup, + fleetAgents: fleetAgents.map((agent) => { + const packagePolicy = policiesGroupedById[agent.id]; + const apmServerCompiledInputs = + packagePolicy.inputs[0].compiled_input['apm-server']; + return { + id: agent.id, + name: agent.name, + apmServerUrl: apmServerCompiledInputs?.url, + secretToken: apmServerCompiledInputs?.secret_token, + }; + }), + }; + }, +}); + +export const ApmFleetRouteRepository = createApmServerRouteRepository() + .add(hasFleetDataRoute) + .add(fleetAgentsRoute); diff --git a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts index ba11a996f00df..e2bf09dae5bed 100644 --- a/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts +++ b/x-pack/plugins/apm/server/tutorial/instructions/apm_agent_instructions.ts @@ -31,55 +31,7 @@ export const createNodeAgentInstructions = ( APM services are created programmatically based on the `serviceName`. \ This agent supports a variety of frameworks but can also be used with your custom stack.', }), - commands: `// ${i18n.translate( - 'xpack.apm.tutorial.nodeClient.configure.commands.addThisToTheFileTopComment', - { - defaultMessage: - 'Add this to the VERY top of the first file loaded in your app', - } - )} -var apm = require('elastic-apm-node').start({curlyOpen} - - // ${i18n.translate( - 'xpack.apm.tutorial.nodeClient.configure.commands.setRequiredServiceNameComment', - { - defaultMessage: 'Override the service name from package.json', - } - )} - // ${i18n.translate( - 'xpack.apm.tutorial.nodeClient.configure.commands.allowedCharactersComment', - { - defaultMessage: 'Allowed characters: a-z, A-Z, 0-9, -, _, and space', - } - )} - serviceName: '', - - // ${i18n.translate( - 'xpack.apm.tutorial.nodeClient.configure.commands.useIfApmRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} - secretToken: '${secretToken}', - - // ${i18n.translate( - 'xpack.apm.tutorial.nodeClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: - 'Set the custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - serverUrl: '${apmServerUrl}', - - // ${i18n.translate( - 'xpack.apm.tutorial.nodeClient.configure.commands.setCustomServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - environment: 'production' -{curlyClose})`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.nodeClient.configure.textPost', { @@ -122,70 +74,7 @@ export const createDjangoAgentInstructions = ( APM services are created programmatically based on the `SERVICE_NAME`.', } ), - commands: `# ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.addAgentComment', - { - defaultMessage: 'Add the agent to the installed apps', - } - )} -INSTALLED_APPS = ( - 'elasticapm.contrib.django', - # ... -) - -ELASTIC_APM = {curlyOpen} - # ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.setRequiredServiceNameComment', - { - defaultMessage: 'Set the required service name. Allowed characters:', - } - )} - # ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.allowedCharactersComment', - { - defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', - } - )} - 'SERVICE_NAME': '', - - # ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.useIfApmServerRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} - 'SECRET_TOKEN': '${secretToken}', - - # ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: - 'Set the custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - 'SERVER_URL': '${apmServerUrl}', - - # ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.setServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - 'ENVIRONMENT': 'production', -{curlyClose} - -# ${i18n.translate( - 'xpack.apm.tutorial.djangoClient.configure.commands.addTracingMiddlewareComment', - { - defaultMessage: - 'To send performance metrics, add our tracing middleware:', - } - )} -MIDDLEWARE = ( - 'elasticapm.contrib.django.middleware.TracingMiddleware', - #... -)`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.djangoClient.configure.textPost', { @@ -225,67 +114,7 @@ export const createFlaskAgentInstructions = ( APM services are created programmatically based on the `SERVICE_NAME`.', } ), - commands: `# ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.initializeUsingEnvironmentVariablesComment', - { - defaultMessage: 'initialize using environment variables', - } - )} -from elasticapm.contrib.flask import ElasticAPM -app = Flask(__name__) -apm = ElasticAPM(app) - -# ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.configureElasticApmComment', - { - defaultMessage: - "or configure to use ELASTIC_APM in your application's settings", - } - )} -from elasticapm.contrib.flask import ElasticAPM -app.config['ELASTIC_APM'] = {curlyOpen} - # ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.setRequiredServiceNameComment', - { - defaultMessage: 'Set the required service name. Allowed characters:', - } - )} - # ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.allowedCharactersComment', - { - defaultMessage: 'a-z, A-Z, 0-9, -, _, and space', - } - )} - 'SERVICE_NAME': '', - - # ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.useIfApmServerRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} - 'SECRET_TOKEN': '${secretToken}', - - # ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: - 'Set the custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - 'SERVER_URL': '${apmServerUrl}', - - # ${i18n.translate( - 'xpack.apm.tutorial.flaskClient.configure.commands.setServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - 'ENVIRONMENT': 'production', -{curlyClose} - -apm = ElasticAPM(app)`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.flaskClient.configure.textPost', { @@ -325,20 +154,7 @@ export const createRailsAgentInstructions = ( values: { configFile: '`config/elastic_apm.yml`' }, } ), - commands: `# config/elastic_apm.yml: - -# Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space -# Defaults to the name of your Rails app -service_name: 'my-service' - -# Use if APM Server requires a secret token -secret_token: '${secretToken}' - -# Set the custom APM Server URL (default: http://localhost:8200) -server_url: '${apmServerUrl || 'http://localhost:8200'}' - -# Set the service environment -environment: 'production'`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.railsClient.configure.textPost', { @@ -413,48 +229,7 @@ export const createRackAgentInstructions = ( values: { configFile: '`config/elastic_apm.yml`' }, } ), - commands: `# config/elastic_apm.yml: - -# ${i18n.translate( - 'xpack.apm.tutorial.rackClient.createConfig.commands.setServiceNameComment', - { - defaultMessage: - 'Set the service name - allowed characters: a-z, A-Z, 0-9, -, _ and space', - } - )} -# ${i18n.translate( - 'xpack.apm.tutorial.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', - { - defaultMessage: "Defaults to the name of your Rack app's class.", - } - )} -service_name: 'my-service' - -# ${i18n.translate( - 'xpack.apm.tutorial.rackClient.createConfig.commands.useIfApmServerRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a token', - } - )} -secret_token: '${secretToken}' - -# ${i18n.translate( - 'xpack.apm.tutorial.rackClient.createConfig.commands.setCustomApmServerComment', - { - defaultMessage: - 'Set custom APM Server URL (default: {defaultServerUrl})', - values: { defaultServerUrl: 'http://localhost:8200' }, - } - )} -server_url: '${apmServerUrl || 'http://localhost:8200'}', - -# ${i18n.translate( - 'xpack.apm.tutorial.rackClient.createConfig.commands.setServiceEnvironment', - { - defaultMessage: 'Set the service environment', - } - )} -environment: 'production'`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.rackClient.createConfig.textPost', { @@ -506,45 +281,7 @@ for details on how to enable RUM support.', The Agent can then be initialized and configured in your application like this:', } ), - commands: `import {curlyOpen} init as initApm {curlyClose} from '@elastic/apm-rum' -var apm = initApm({curlyOpen} - - // ${i18n.translate( - 'xpack.apm.tutorial.jsClient.installDependency.commands.setRequiredServiceNameComment', - { - defaultMessage: - 'Set required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)', - } - )} - serviceName: 'your-app-name', - - // ${i18n.translate( - 'xpack.apm.tutorial.jsClient.installDependency.commands.setCustomApmServerUrlComment', - { - defaultMessage: - 'Set custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} - serverUrl: '${apmServerUrl}', - - // ${i18n.translate( - 'xpack.apm.tutorial.jsClient.installDependency.commands.setServiceVersionComment', - { - defaultMessage: - 'Set the service version (required for source map feature)', - } - )} - serviceVersion: '', - - // ${i18n.translate( - 'xpack.apm.tutorial.jsClient.installDependency.commands.setServiceEnvironmentComment', - { - defaultMessage: 'Set the service environment', - } - )} - environment: 'production' -{curlyClose})`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.jsClient.installDependency.textPost', { @@ -575,15 +312,7 @@ and host the file on your Server/CDN before deploying to production.", 'https://unpkg.com/@elastic/apm-rum/dist/bundles/elastic-apm-rum.umd.min.js', }, }), - commands: `\ - - -`.split('\n'), + customComponentName: 'TutorialConfigAgentRumScript', }, ]; @@ -610,55 +339,7 @@ export const createGoAgentInstructions = ( APM services are created programmatically based on the executable \ file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.', }), - commands: `# ${i18n.translate( - 'xpack.apm.tutorial.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', - { - defaultMessage: 'Initialize using environment variables:', - } - )} - -# ${i18n.translate( - 'xpack.apm.tutorial.goClient.configure.commands.setServiceNameComment', - { - defaultMessage: - 'Set the service name. Allowed characters: # a-z, A-Z, 0-9, -, _, and space.', - } - )} -# ${i18n.translate( - 'xpack.apm.tutorial.goClient.configure.commands.usedExecutableNameComment', - { - defaultMessage: - 'If ELASTIC_APM_SERVICE_NAME is not specified, the executable name will be used.', - } - )} -export ELASTIC_APM_SERVICE_NAME= - -# ${i18n.translate( - 'xpack.apm.tutorial.goClient.configure.commands.setCustomApmServerUrlComment', - { - defaultMessage: - 'Set custom APM Server URL (default: {defaultApmServerUrl})', - values: { defaultApmServerUrl: 'http://localhost:8200' }, - } - )} -export ELASTIC_APM_SERVER_URL=${apmServerUrl} - -# ${i18n.translate( - 'xpack.apm.tutorial.goClient.configure.commands.useIfApmRequiresTokenComment', - { - defaultMessage: 'Use if APM Server requires a secret token', - } - )} -export ELASTIC_APM_SECRET_TOKEN=${secretToken} - -# ${i18n.translate( - 'xpack.apm.tutorial.goClient.configure.commands.setServiceEnvironment', - { - defaultMessage: 'Set the service environment', - } - )} -export ELASTIC_APM_ENVIRONMENT= -`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate('xpack.apm.tutorial.goClient.configure.textPost', { defaultMessage: 'See the [documentation]({documentationLink}) for advanced configuration.', @@ -743,13 +424,7 @@ Do **not** add the agent as a dependency to your application.', values: { customApmServerUrl: 'http://localhost:8200' }, } ), - commands: `java -javaagent:/path/to/elastic-apm-agent-.jar \\ - -Delastic.apm.service_name=my-application \\ - -Delastic.apm.server_urls=${apmServerUrl || 'http://localhost:8200'} \\ - -Delastic.apm.secret_token=${secretToken} \\ - -Delastic.apm.environment=production \\ - -Delastic.apm.application_packages=org.example \\ - -jar my-application.jar`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.javaClient.startApplication.textPost', { @@ -837,16 +512,7 @@ export const createDotNetAgentInstructions = ( defaultMessage: 'Sample appsettings.json file:', } ), - commands: `{curlyOpen} - "ElasticApm": {curlyOpen} - "SecretToken": "${secretToken}", - "ServerUrls": "${ - apmServerUrl || 'http://localhost:8200' - }", //Set custom APM Server URL (default: http://localhost:8200) - "ServiceName": "MyApp", //allowed characters: a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application - "Environment": "production", // Set the service environment - {curlyClose} -{curlyClose}`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.dotNetClient.configureAgent.textPost', { @@ -913,12 +579,7 @@ export const createPhpAgentInstructions = ( 'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:', } ), - commands: `elastic_apm.server_url="${ - apmServerUrl || 'http://localhost:8200' - }" -elastic.apm.secret_token="${secretToken}" -elastic_apm.service_name="My service" -`.split('\n'), + customComponentName: 'TutorialConfigAgent', textPost: i18n.translate( 'xpack.apm.tutorial.phpClient.configure.textPost', { diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 43a5a14b425b5..2632b7f9dd85a 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -88,6 +88,7 @@ export const createMockAgentPolicyService = (): jest.Mocked { diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index ebddb695d695b..f82415987e5ac 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -69,6 +69,7 @@ export interface AgentPolicyServiceInterface { list: typeof agentPolicyService['list']; getDefaultAgentPolicyId: typeof agentPolicyService['getDefaultAgentPolicyId']; getFullAgentPolicy: typeof agentPolicyService['getFullAgentPolicy']; + getByIds: typeof agentPolicyService['getByIDs']; } // Saved object services