diff --git a/config/serverless.oblt.yml b/config/serverless.oblt.yml index 7687537296771..df251857c9575 100644 --- a/config/serverless.oblt.yml +++ b/config/serverless.oblt.yml @@ -19,3 +19,11 @@ xpack.serverless.plugin.developer.projectSwitcher.currentType: 'observability' ## Disable adding the component template `.fleet_agent_id_verification-1` to every index template for each datastream for each integration xpack.fleet.agentIdVerificationEnabled: false + +## APM Serverless Onboarding flow +xpack.apm.serverlessOnboarding: true + +## Required for force installation of APM Package +xpack.fleet.packages: + - name: apm + version: latest diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 272a53eb7b283..fc5be23eba61d 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -177,6 +177,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.apm.serviceMapEnabled (boolean)', 'xpack.apm.ui.enabled (boolean)', 'xpack.apm.ui.maxTraceItems (number)', + 'xpack.apm.managedServiceUrl (any)', + 'xpack.apm.serverlessOnboarding (any)', 'xpack.apm.latestAgentVersionsUrl (string)', 'xpack.cases.files.allowedMimeTypes (array)', 'xpack.cases.files.maxSize (number)', diff --git a/x-pack/plugins/apm/common/tutorial/tutorials.ts b/x-pack/plugins/apm/common/tutorial/tutorials.ts new file mode 100644 index 0000000000000..a401ec4bb15f3 --- /dev/null +++ b/x-pack/plugins/apm/common/tutorial/tutorials.ts @@ -0,0 +1,32 @@ +/* + * 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 { CustomIntegration } from '@kbn/custom-integrations-plugin/common'; + +const APM_INTEGRATION_CATEGORIES = ['observability', 'apm']; + +export const apmTutorialCustomIntegration: Omit = { + id: 'apm', + title: i18n.translate('xpack.apm.tutorial.specProvider.name', { + defaultMessage: 'APM', + }), + categories: APM_INTEGRATION_CATEGORIES, + uiInternalPath: '/app/apm/onboarding', + description: i18n.translate('xpack.apm.tutorial.introduction', { + defaultMessage: + 'Collect performance metrics from your applications with Elastic APM.', + }), + icons: [ + { + type: 'eui', + src: 'apmApp', + }, + ], + shipper: 'tutorial', + isBeta: false, +}; diff --git a/x-pack/plugins/apm/kibana.jsonc b/x-pack/plugins/apm/kibana.jsonc index 391f8481e59c7..c00c90afb4852 100644 --- a/x-pack/plugins/apm/kibana.jsonc +++ b/x-pack/plugins/apm/kibana.jsonc @@ -45,6 +45,7 @@ "spaces", "taskManager", "usageCollection", + "customIntegrations", // Move this to requiredPlugins after completely migrating from the Tutorials Home App "licenseManagement" ], "requiredBundles": [ diff --git a/x-pack/plugins/apm/public/components/app/onboarding/agent_config_instructions.tsx b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_instructions.tsx new file mode 100644 index 0000000000000..c975d5af42271 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_instructions.tsx @@ -0,0 +1,66 @@ +/* + * 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 React from 'react'; +import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { + getApmAgentCommands, + getApmAgentVariables, + getApmAgentLineNumbers, + getApmAgentHighlightLang, +} from './commands/get_apm_agent_commands'; +import { AgentConfigurationTable } from './agent_config_table'; + +export function AgentConfigInstructions({ + variantId, + apmServerUrl, + secretToken, + apiKey, + createApiKey, + createApiKeyLoading, +}: { + variantId: string; + apmServerUrl: string; + secretToken?: string; + apiKey?: string | null; + createApiKey?: () => void; + createApiKeyLoading?: boolean; +}) { + const commands = getApmAgentCommands({ + variantId, + apmServerUrl, + secretToken, + apiKey, + }); + + const variables = getApmAgentVariables(variantId, secretToken); + const lineNumbers = getApmAgentLineNumbers(variantId, apiKey); + const highlightLang = getApmAgentHighlightLang(variantId); + + return ( + <> + + + + + + {commands} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx new file mode 100644 index 0000000000000..33359b1713ab5 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/agent_config_table.tsx @@ -0,0 +1,106 @@ +/* + * 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 React from 'react'; +import type { ValuesType } from 'utility-types'; +import { get } from 'lodash'; +import { + EuiBasicTable, + EuiText, + EuiBasicTableColumn, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +function ConfigurationValueColumn({ + columnKey, + value, + createApiKey, + createApiKeyLoading, +}: { + columnKey: string; + value: string | null; + createApiKey?: () => void; + createApiKeyLoading?: boolean; +}) { + const shouldRenderCreateApiKeyButton = + columnKey === 'apiKey' && value === null; + + if (shouldRenderCreateApiKeyButton) { + return ( + + {i18n.translate('xpack.apm.tutorial.apiKey.create', { + defaultMessage: 'Create API Key', + })} + + ); + } + + return ( + + {value} + + ); +} + +export function AgentConfigurationTable({ + variables, + data, + createApiKey, + createApiKeyLoading, +}: { + variables: { [key: string]: string }; + data: { + apmServerUrl?: string; + secretToken?: string; + apiKey?: string | null; + }; + createApiKey?: () => void; + createApiKeyLoading?: boolean; +}) { + if (!variables) return null; + + const defaultValues = { + apmServiceName: 'my-service-name', + apmEnvironment: 'my-environment', + }; + + const columns: Array>> = [ + { + field: 'setting', + name: i18n.translate('xpack.apm.onboarding.agent.column.configSettings', { + defaultMessage: 'Configuration setting', + }), + }, + { + field: 'value', + name: i18n.translate('xpack.apm.onboarding.agent.column.configValue', { + defaultMessage: 'Configuration value', + }), + render: (_, { value, key }) => ( + + ), + }, + ]; + + const items = Object.entries(variables).map(([key, value]) => ({ + setting: value, + value: get({ ...data, ...defaultValues }, key), + key, + })); + return ; +} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/django.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/django.ts new file mode 100644 index 0000000000000..993138c69110e --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/django.ts @@ -0,0 +1,65 @@ +/* + * 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 djangoVariables = (secretToken?: string) => ({ + apmServiceName: 'SERVICE_NAME', + ...(secretToken && { secretToken: 'SECRET_TOKEN' }), + ...(!secretToken && { apiKey: 'API_KEY' }), + apmServerUrl: 'SERVER_URL', + apmEnvironment: 'ENVIRONMENT', +}); + +export const djangoHighlightLang = 'py'; + +export const djangoLineNumbers = () => ({ + start: 1, + highlight: '1, 3, 5, 7, 9, 12, 15, 18-19, 21, 23, 25', +}); + +export const django = `INSTALLED_APPS = ( + # ${i18n.translate( + 'xpack.apm.onboarding.djangoClient.configure.commands.addAgentComment', + { + defaultMessage: 'Add the agent to installed apps', + } + )} + 'elasticapm.contrib.django', + # ... +) + +ELASTIC_APM = { + # {{serviceNameHint}} + 'SERVICE_NAME': 'my-service-name', + + {{^secretToken}} + # {{apiKeyHint}} + 'API_KEY': '{{{apiKey}}}', + {{/secretToken}} + {{#secretToken}} + # {{secretTokenHint}} + 'SECRET_TOKEN': '{{{secretToken}}}', + {{/secretToken}} + + # {{{serverUrlHint}}} + 'SERVER_URL': '{{{apmServerUrl}}}', + + # {{{serviceEnvironmentHint}}} + 'ENVIRONMENT': 'my-environment', +} + +MIDDLEWARE = ( + # ${i18n.translate( + 'xpack.apm.onboarding.djangoClient.configure.commands.addTracingMiddlewareComment', + { + defaultMessage: 'Add our tracing middleware to send performance metrics', + } + )} + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... +)`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/dotnet.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/dotnet.ts new file mode 100644 index 0000000000000..84c1b2a3f2603 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/dotnet.ts @@ -0,0 +1,47 @@ +/* + * 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 dotnetVariables = (secretToken?: string) => ({ + apmServiceName: 'ServiceName', + ...(secretToken && { secretToken: 'SecretToken' }), + ...(!secretToken && { apiKey: 'ApiKey' }), + apmServerUrl: 'ServerUrl', + apmEnvironment: 'Environment', +}); + +export const dotnetHighlightLang = 'dotnet'; + +export const dotnetLineNumbers = () => ({ + start: 1, + highlight: '1-2, 4, 6, 8, 10-12', +}); + +export const dotnet = `{ + "ElasticApm": { + /// {{serviceNameHint}} ${i18n.translate( + 'xpack.apm.onboarding.dotnetClient.createConfig.commands.defaultServiceName', + { + defaultMessage: 'Default is the entry assembly of the application.', + } + )} + "ServiceName": "my-service-name", + {{^secretToken}} + /// {{apiKeyHint}} + "ApiKey": "{{{apiKey}}}", + {{/secretToken}} + {{#secretToken}} + /// {{secretTokenHint}} + "SecretToken": "{{{secretToken}}}", + {{/secretToken}} + /// {{{serverUrlHint}}} + "ServerUrl": "{{{apmServerUrl}}}", + /// {{{serviceEnvironmentHint}}} + "Environment": "my-environment", + } +}`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/flask.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/flask.ts new file mode 100644 index 0000000000000..a95d64a11a318 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/flask.ts @@ -0,0 +1,62 @@ +/* + * 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 flaskVariables = (secretToken?: string) => ({ + apmServiceName: 'SERVICE_NAME', + ...(secretToken && { secretToken: 'SECRET_TOKEN' }), + ...(!secretToken && { apiKey: 'API_KEY' }), + apmServerUrl: 'SERVER_URL', + apmEnvironment: 'ENVIRONMENT', +}); + +export const flaskHighlightLang = 'py'; + +export const flaskLineNumbers = () => ({ + start: 1, + highlight: '2-4, 7-8, 10, 13, 16, 19-22', +}); + +export const flask = `# ${i18n.translate( + 'xpack.apm.onboarding.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.onboarding.flaskClient.configure.commands.configureElasticApmComment', + { + defaultMessage: "Or use ELASTIC_APM in your application's settings", + } +)} +from elasticapm.contrib.flask import ElasticAPM +app.config['ELASTIC_APM'] = { + # {{serviceNameHint}} + 'SERVICE_NAME': 'my-service-name', + + {{^secretToken}} + # {{apiKeyHint}} + 'API_KEY': '{{{apiKey}}}', + {{/secretToken}} + {{#secretToken}} + # {{secretTokenHint}} + 'SECRET_TOKEN': '{{{secretToken}}}', + {{/secretToken}} + + # {{{serverUrlHint}}} + 'SERVER_URL': '{{{apmServerUrl}}}', + + {{{serviceEnvironmentHint}}} + 'ENVIRONMENT': 'my-environment', +} + +apm = ElasticAPM(app)`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/get_apm_agent_commands.test.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/get_apm_agent_commands.test.ts new file mode 100644 index 0000000000000..2ff78a0321e93 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/get_apm_agent_commands.test.ts @@ -0,0 +1,696 @@ +/* + * 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 { getApmAgentCommands } from './get_apm_agent_commands'; + +describe('getCommands', () => { + describe('Unknown agent', () => { + it('renders empty command', () => { + const commands = getApmAgentCommands({ + variantId: 'foo', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).toBe(''); + }); + }); + describe('Java agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'java', + }); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-.jar \\\\ + -Delastic.apm.service_name=my-service-name \\\\ + -Delastic.apm.api_key= \\\\ + -Delastic.apm.server_url= \\\\ + -Delastic.apm.environment=my-environment \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-service-name.jar" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'java', + 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-service-name \\\\ + -Delastic.apm.secret_token=foobar \\\\ + -Delastic.apm.server_url=localhost:8220 \\\\ + -Delastic.apm.environment=my-environment \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-service-name.jar" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'java', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "java -javaagent:/path/to/elastic-apm-agent-.jar \\\\ + -Delastic.apm.service_name=my-service-name \\\\ + -Delastic.apm.secret_token=foobar \\\\ + -Delastic.apm.server_url=localhost:8220 \\\\ + -Delastic.apm.environment=my-environment \\\\ + -Delastic.apm.application_packages=org.example \\\\ + -jar my-service-name.jar" + `); + }); + }); + + describe('Node.js agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'node', + }); + 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({ + + // The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Overrides the service name in package.json. + serviceName: 'my-service-name', + + // Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + apiKey: '', + + // Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + serverUrl: '', + + // The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment' + })" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'node', + 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({ + + // The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Overrides the service name in package.json. + serviceName: 'my-service-name', + + // Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + secretToken: 'foobar', + + // Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + serverUrl: 'localhost:8220', + + // The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment' + })" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'node', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + 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({ + + // The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Overrides the service name in package.json. + serviceName: 'my-service-name', + + // Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + secretToken: 'foobar', + + // Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + serverUrl: 'localhost:8220', + + // The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment' + })" + `); + }); + }); + describe('Django agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'django', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "INSTALLED_APPS = ( + # Add the agent to installed apps + 'elasticapm.contrib.django', + # ... + ) + + ELASTIC_APM = { + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + 'SERVICE_NAME': 'my-service-name', + + # Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + 'API_KEY': '', + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + 'SERVER_URL': '', + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + 'ENVIRONMENT': 'my-environment', + } + + MIDDLEWARE = ( + # Add our tracing middleware to send performance metrics + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... + )" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'django', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "INSTALLED_APPS = ( + # Add the agent to installed apps + 'elasticapm.contrib.django', + # ... + ) + + ELASTIC_APM = { + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + 'SERVICE_NAME': 'my-service-name', + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + 'SECRET_TOKEN': 'foobar', + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + 'SERVER_URL': 'localhost:8220', + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + 'ENVIRONMENT': 'my-environment', + } + + MIDDLEWARE = ( + # Add our tracing middleware to send performance metrics + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... + )" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'django', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "INSTALLED_APPS = ( + # Add the agent to installed apps + 'elasticapm.contrib.django', + # ... + ) + + ELASTIC_APM = { + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + 'SERVICE_NAME': 'my-service-name', + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + 'SECRET_TOKEN': 'foobar', + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + 'SERVER_URL': 'localhost:8220', + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + 'ENVIRONMENT': 'my-environment', + } + + MIDDLEWARE = ( + # Add our tracing middleware to send performance metrics + 'elasticapm.contrib.django.middleware.TracingMiddleware', + #... + )" + `); + }); + }); + describe('Flask agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'flask', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables + from elasticapm.contrib.flask import ElasticAPM + app = Flask(__name__) + apm = ElasticAPM(app) + + # Or use ELASTIC_APM in your application's settings + from elasticapm.contrib.flask import ElasticAPM + app.config['ELASTIC_APM'] = { + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + 'SERVICE_NAME': 'my-service-name', + + # Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + 'API_KEY': '', + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + 'SERVER_URL': '', + + The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + 'ENVIRONMENT': 'my-environment', + } + + apm = ElasticAPM(app)" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'flask', + 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 use ELASTIC_APM in your application's settings + from elasticapm.contrib.flask import ElasticAPM + app.config['ELASTIC_APM'] = { + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + 'SERVICE_NAME': 'my-service-name', + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + 'SECRET_TOKEN': 'foobar', + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + 'SERVER_URL': 'localhost:8220', + + The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + 'ENVIRONMENT': 'my-environment', + } + + apm = ElasticAPM(app)" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'flask', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables + from elasticapm.contrib.flask import ElasticAPM + app = Flask(__name__) + apm = ElasticAPM(app) + + # Or use ELASTIC_APM in your application's settings + from elasticapm.contrib.flask import ElasticAPM + app.config['ELASTIC_APM'] = { + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + 'SERVICE_NAME': 'my-service-name', + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + 'SECRET_TOKEN': 'foobar', + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + 'SERVER_URL': 'localhost:8220', + + The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + 'ENVIRONMENT': 'my-environment', + } + + apm = ElasticAPM(app)" + `); + }); + }); + describe('Ruby on Rails agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'rails', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Defaults to the name of your Rails app. + service_name: 'my-service-name' + + # Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + api_key: '' + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + server_url: '' + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment'" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'rails', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Defaults to the name of your Rails app. + service_name: 'my-service-name' + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + secret_token: 'foobar' + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + server_url: 'localhost:8220' + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment'" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'rails', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Defaults to the name of your Rails app. + service_name: 'my-service-name' + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + secret_token: 'foobar' + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + server_url: 'localhost:8220' + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment'" + `); + }); + }); + describe('Rack agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'rack', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Defaults to the name of your Rack app's class. + service_name: 'my-service-name' + + # Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + api_key: '' + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + server_url: '' + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment'" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'rack', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Defaults to the name of your Rack app's class. + service_name: 'my-service-name' + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + secret_token: 'foobar' + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + server_url: 'localhost:8220' + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment'" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'rack', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# config/elastic_apm.yml: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Defaults to the name of your Rack app's class. + service_name: 'my-service-name' + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + secret_token: 'foobar' + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + server_url: 'localhost:8220' + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + environment: 'my-environment'" + `); + }); + }); + describe('Go agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'go', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. If not specified, the executable name will be used. + export ELASTIC_APM_SERVICE_NAME=my-service-name + + # Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + export ELASTIC_APM_API_KEY= + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + export ELASTIC_APM_SERVER_URL= + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + export ELASTIC_APM_ENVIRONMENT=my-environment + " + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'go', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. If not specified, the executable name will be used. + export ELASTIC_APM_SERVICE_NAME=my-service-name + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + export ELASTIC_APM_SECRET_TOKEN=foobar + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + export ELASTIC_APM_SERVER_URL=localhost:8220 + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + export ELASTIC_APM_ENVIRONMENT=my-environment + " + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'go', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# Initialize using environment variables: + + # The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. If not specified, the executable name will be used. + export ELASTIC_APM_SERVICE_NAME=my-service-name + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + export ELASTIC_APM_SECRET_TOKEN=foobar + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + export ELASTIC_APM_SERVER_URL=localhost:8220 + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + export ELASTIC_APM_ENVIRONMENT=my-environment + " + `); + }); + }); + describe('DotNet agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'dotnet', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "{ + \\"ElasticApm\\": { + /// The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application. + \\"ServiceName\\": \\"my-service-name\\", + /// Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + \\"ApiKey\\": \\"\\", + /// Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + \\"ServerUrl\\": \\"\\", + /// The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + \\"Environment\\": \\"my-environment\\", + } + }" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'dotnet', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "{ + \\"ElasticApm\\": { + /// The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application. + \\"ServiceName\\": \\"my-service-name\\", + /// Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + \\"SecretToken\\": \\"foobar\\", + /// Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + \\"ServerUrl\\": \\"localhost:8220\\", + /// The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + \\"Environment\\": \\"my-environment\\", + } + }" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'dotnet', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "{ + \\"ElasticApm\\": { + /// The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. Default is the entry assembly of the application. + \\"ServiceName\\": \\"my-service-name\\", + /// Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + \\"SecretToken\\": \\"foobar\\", + /// Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + \\"ServerUrl\\": \\"localhost:8220\\", + /// The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + \\"Environment\\": \\"my-environment\\", + } + }" + `); + }); + }); + describe('PHP agent', () => { + it('renders empty commands', () => { + const commands = getApmAgentCommands({ + variantId: 'php', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + elastic_apm.service_name=\\"my-service-name\\" + + # Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored. + elastic_apm.api_key=\\"\\" + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + elastic_apm.server_url=\\"\\" + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + elastic_apm.environment=\\"my-environment\\"" + `); + }); + it('renders with secret token and url', () => { + const commands = getApmAgentCommands({ + variantId: 'php', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + elastic_apm.service_name=\\"my-service-name\\" + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + elastic_apm.secret_token=\\"foobar\\" + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + elastic_apm.server_url=\\"localhost:8220\\" + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + elastic_apm.environment=\\"my-environment\\"" + `); + }); + it('renders with api key even though secret token is present', () => { + const commands = getApmAgentCommands({ + variantId: 'php', + apmServerUrl: 'localhost:8220', + secretToken: 'foobar', + apiKey: 'myApiKey', + }); + expect(commands).not.toBe(''); + expect(commands).toMatchInlineSnapshot(` + "# The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space. + elastic_apm.service_name=\\"my-service-name\\" + + # Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server. + elastic_apm.secret_token=\\"foobar\\" + + # Set the custom APM Server URL (default: http://localhost:8200). The URL must be fully qualified, including protocol (http or https) and port. + elastic_apm.server_url=\\"localhost:8220\\" + + # The name of the environment this service is deployed in, e.g., \\"production\\" or \\"staging\\". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents. + elastic_apm.environment=\\"my-environment\\"" + `); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/get_apm_agent_commands.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/get_apm_agent_commands.ts new file mode 100644 index 0000000000000..e2eb92e196cce --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/get_apm_agent_commands.ts @@ -0,0 +1,161 @@ +/* + * 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, + javaVariables, + javaLineNumbers, + javaHighlightLang, +} from './java'; +import { + node, + nodeVariables, + nodeLineNumbers, + nodeHighlightLang, +} from './node'; +import { + django, + djangoVariables, + djangoLineNumbers, + djangoHighlightLang, +} from './django'; +import { + flask, + flaskVariables, + flaskLineNumbers, + flaskHighlightLang, +} from './flask'; +import { + rails, + railsVariables, + railsLineNumbers, + railsHighlightLang, +} from './rails'; +import { + rack, + rackVariables, + rackLineNumbers, + rackHighlightLang, +} from './rack'; +import { go, goVariables, goLineNumbers, goHighlightLang } from './go'; +import { + dotnet, + dotnetVariables, + dotnetLineNumbers, + dotnetHighlightLang, +} from './dotnet'; +import { php, phpVariables, phpLineNumbers, phpHighlightLang } from './php'; +import { + serviceNameHint, + serviceEnvironmentHint, + serverUrlHint, + secretTokenHint, + apiKeyHint, +} from './shared_hints'; + +const apmAgentCommandsMap: Record = { + java, + node, + django, + flask, + rails, + rack, + go, + dotnet, + php, +}; + +interface Variables { + [key: string]: string; +} + +const apmAgentVariablesMap: ( + secretToken?: string +) => Record = (secretToken?: string) => ({ + java: javaVariables(secretToken), + node: nodeVariables(secretToken), + django: djangoVariables(secretToken), + flask: flaskVariables(secretToken), + rails: railsVariables(secretToken), + rack: rackVariables(secretToken), + go: goVariables(secretToken), + dotnet: dotnetVariables(secretToken), + php: phpVariables(secretToken), +}); + +interface LineNumbers { + [key: string]: string | number | object; +} + +const apmAgentLineNumbersMap: ( + apiKey?: string | null +) => Record = (apiKey?: string | null) => ({ + java: javaLineNumbers(apiKey), + node: nodeLineNumbers(), + django: djangoLineNumbers(), + flask: flaskLineNumbers(), + rails: railsLineNumbers(), + rack: rackLineNumbers(), + go: goLineNumbers(), + dotnet: dotnetLineNumbers(), + php: phpLineNumbers(), +}); + +const apmAgentHighlightLangMap: Record = { + java: javaHighlightLang, + node: nodeHighlightLang, + django: djangoHighlightLang, + flask: flaskHighlightLang, + rails: railsHighlightLang, + rack: rackHighlightLang, + go: goHighlightLang, + dotnet: dotnetHighlightLang, + php: phpHighlightLang, +}; + +export function getApmAgentCommands({ + variantId, + apmServerUrl, + secretToken, + apiKey, +}: { + variantId: string; + apmServerUrl?: string; + secretToken?: string; + apiKey?: string | null; +}) { + const commands = apmAgentCommandsMap[variantId]; + if (!commands) { + return ''; + } + + return Mustache.render(commands, { + apmServerUrl, + secretToken, + apiKey, + serviceNameHint, + serviceEnvironmentHint, + serverUrlHint, + secretTokenHint, + apiKeyHint, + }); +} + +export function getApmAgentVariables(variantId: string, secretToken?: string) { + return apmAgentVariablesMap(secretToken)[variantId]; +} + +export function getApmAgentLineNumbers( + variantId: string, + apiKey?: string | null +) { + return apmAgentLineNumbersMap(apiKey)[variantId]; +} + +export function getApmAgentHighlightLang(variantId: string) { + return apmAgentHighlightLangMap[variantId]; +} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/go.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/go.ts new file mode 100644 index 0000000000000..c95cb30606c85 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/go.ts @@ -0,0 +1,54 @@ +/* + * 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 goVariables = (secretToken?: string) => ({ + apmServiceName: 'ELASTIC_APM_SERVICE_NAME', + ...(secretToken && { secretToken: 'ELASTIC_APM_SECRET_TOKEN' }), + ...(!secretToken && { apiKey: 'ELASTIC_APM_API_KEY' }), + apmServerUrl: 'ELASTIC_APM_SERVER_URL', + apmEnvironment: 'ELASTIC_APM_ENVIRONMENT', +}); + +export const goHighlightLang = 'go'; + +export const goLineNumbers = () => ({ + start: 1, + highlight: '4, 7, 10, 13', +}); + +export const go = `# ${i18n.translate( + 'xpack.apm.onboarding.goClient.configure.commands.initializeUsingEnvironmentVariablesComment', + { + defaultMessage: 'Initialize using environment variables:', + } +)} + +# {{serviceNameHint}} ${i18n.translate( + 'xpack.apm.onboarding.goClient.configure.commands.usedExecutableNameComment', + { + defaultMessage: 'If not specified, the executable name will be used.', + } +)} +export ELASTIC_APM_SERVICE_NAME=my-service-name + +{{^secretToken}} +# {{apiKeyHint}} +export ELASTIC_APM_API_KEY={{{apiKey}}} +{{/secretToken}} +{{#secretToken}} +# {{secretTokenHint}} +export ELASTIC_APM_SECRET_TOKEN={{{secretToken}}} +{{/secretToken}} + +# {{{serverUrlHint}}} +export ELASTIC_APM_SERVER_URL={{{apmServerUrl}}} + +# {{{serviceEnvironmentHint}}} +export ELASTIC_APM_ENVIRONMENT=my-environment +`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/java.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/java.ts new file mode 100644 index 0000000000000..f093b0b241af0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/java.ts @@ -0,0 +1,47 @@ +/* + * 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 { + serviceNameHint, + secretTokenHint, + serverUrlHint, + serviceEnvironmentHint, + apiKeyHint, +} from './shared_hints'; + +export const javaVariables = (secretToken?: string) => ({ + apmServiceName: 'Delastic.apm.service_name', + ...(secretToken && { secretToken: 'Delastic.apm.secret_token' }), + ...(!secretToken && { apiKey: 'Delastic.apm.api_key' }), + apmServerUrl: 'Delastic.apm.server_url', + apmEnvironment: 'Delastic.apm.environment', +}); + +export const javaHighlightLang = 'java'; + +export const javaLineNumbers = (apiKey?: string | null) => ({ + start: 1, + highlight: '', + annotations: { + 2: serviceNameHint, + 3: apiKey ? apiKeyHint : secretTokenHint, + 4: serverUrlHint, + 5: serviceEnvironmentHint, + }, +}); +export const java = `java -javaagent:/path/to/elastic-apm-agent-.jar \\ +-Delastic.apm.service_name=my-service-name \\ +{{^secretToken}} +-Delastic.apm.api_key={{{apiKey}}} \\ +{{/secretToken}} +{{#secretToken}} +-Delastic.apm.secret_token={{{secretToken}}} \\ +{{/secretToken}} +-Delastic.apm.server_url={{{apmServerUrl}}} \\ +-Delastic.apm.environment=my-environment \\ +-Delastic.apm.application_packages=org.example \\ +-jar my-service-name.jar`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/node.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/node.ts new file mode 100644 index 0000000000000..97e2ab0b43809 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/node.ts @@ -0,0 +1,55 @@ +/* + * 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 nodeVariables = (secretToken?: string) => ({ + apmServiceName: 'serviceName', + ...(secretToken && { secretToken: 'secretToken' }), + ...(!secretToken && { apiKey: 'apiKey' }), + apmServerUrl: 'serverUrl', + apmEnvironment: 'environment', +}); + +export const nodeHighlightLang = 'js'; + +export const nodeLineNumbers = () => ({ + start: 1, + highlight: '2, 5, 8, 11, 14-15', +}); + +export const node = `// ${i18n.translate( + 'xpack.apm.onboarding.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({ + + // {{serviceNameHint}} ${i18n.translate( + 'xpack.apm.onboarding.nodeClient.createConfig.commands.serviceName', + { + defaultMessage: 'Overrides the service name in package.json.', + } + )} + serviceName: 'my-service-name', + + {{^secretToken}} + // {{apiKeyHint}} + apiKey: '{{{apiKey}}}', + {{/secretToken}} + {{#secretToken}} + // {{secretTokenHint}} + secretToken: '{{{secretToken}}}', + {{/secretToken}} + + // {{{serverUrlHint}}} + serverUrl: '{{{apmServerUrl}}}', + + // {{{serviceEnvironmentHint}}} + environment: 'my-environment' +})`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/php.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/php.ts new file mode 100644 index 0000000000000..c00fea44dc998 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/php.ts @@ -0,0 +1,38 @@ +/* + * 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 phpVariables = (secretToken?: string) => ({ + apmServiceName: 'elastic_apm.service_name', + ...(secretToken && { secretToken: 'elastic_apm.secret_token' }), + ...(!secretToken && { apiKey: 'elastic_apm.api_key' }), + apmServerUrl: 'elastic_apm.server_url', + apmEnvironment: 'elastic_apm.environment', +}); + +export const phpHighlightLang = 'php'; + +export const phpLineNumbers = () => ({ + start: 1, + highlight: '2, 5, 8, 11', +}); + +export const php = `# {{serviceNameHint}} +elastic_apm.service_name="my-service-name" + +{{^secretToken}} +# {{apiKeyHint}} +elastic_apm.api_key="{{{apiKey}}}" +{{/secretToken}} +{{#secretToken}} +# {{secretTokenHint}} +elastic_apm.secret_token="{{{secretToken}}}" +{{/secretToken}} + +# {{serverUrlHint}} +elastic_apm.server_url="{{{apmServerUrl}}}" + +# {{{serviceEnvironmentHint}}} +elastic_apm.environment="my-environment"`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/rack.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/rack.ts new file mode 100644 index 0000000000000..54d484d92c513 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/rack.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 { i18n } from '@kbn/i18n'; + +export const rackVariables = (secretToken?: string) => ({ + apmServiceName: 'service_name', + ...(secretToken && { secretToken: 'secret_token' }), + ...(!secretToken && { apiKey: 'api_key' }), + apmServerUrl: 'server_url', + apmEnvironment: 'environment', +}); + +export const rackHighlightLang = 'rb'; + +export const rackLineNumbers = () => ({ + start: 1, + highlight: '4, 7, 10, 13', +}); + +export const rack = `# config/elastic_apm.yml: + +# {{serviceNameHint}} ${i18n.translate( + 'xpack.apm.onboarding.rackClient.createConfig.commands.defaultsToTheNameOfRackAppClassComment', + { + defaultMessage: "Defaults to the name of your Rack app's class.", + } +)} +service_name: 'my-service-name' + +{{^secretToken}} +# {{apiKeyHint}} +api_key: '{{{apiKey}}}' +{{/secretToken}} +{{#secretToken}} +# {{secretTokenHint}} +secret_token: '{{{secretToken}}}' +{{/secretToken}} + +# {{{serverUrlHint}}} +server_url: '{{{apmServerUrl}}}' + +# {{{serviceEnvironmentHint}}} +environment: 'my-environment'`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/rails.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/rails.ts new file mode 100644 index 0000000000000..9f7bb313400eb --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/rails.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 { i18n } from '@kbn/i18n'; + +export const railsVariables = (secretToken?: string) => ({ + apmServiceName: 'service_name', + ...(secretToken && { secretToken: 'secret_token' }), + ...(!secretToken && { apiKey: 'api_key' }), + apmServerUrl: 'server_url', + apmEnvironment: 'environment', +}); + +export const railsHighlightLang = 'rb'; + +export const railsLineNumbers = () => ({ + start: 1, + highlight: '4, 7, 10, 13', +}); + +export const rails = `# config/elastic_apm.yml: + +# {{serviceNameHint}} ${i18n.translate( + 'xpack.apm.onboarding.railsClient.createConfig.commands.defaultServiceName', + { + defaultMessage: 'Defaults to the name of your Rails app.', + } +)} +service_name: 'my-service-name' + +{{^secretToken}} +# {{apiKeyHint}} +api_key: '{{{apiKey}}}' +{{/secretToken}} +{{#secretToken}} +# {{secretTokenHint}} +secret_token: '{{{secretToken}}}' +{{/secretToken}} + +# {{{serverUrlHint}}} +server_url: '{{{apmServerUrl}}}' + +# {{{serviceEnvironmentHint}}} +environment: 'my-environment'`; diff --git a/x-pack/plugins/apm/public/components/app/onboarding/commands/shared_hints.ts b/x-pack/plugins/apm/public/components/app/onboarding/commands/shared_hints.ts new file mode 100644 index 0000000000000..bb497d7777498 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/commands/shared_hints.ts @@ -0,0 +1,47 @@ +/* + * 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 serviceNameHint = i18n.translate( + 'xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint', + { + defaultMessage: + 'The service name is the primary filter in the APM UI and is used to group errors and trace data together. Allowed characters are a-z, A-Z, 0-9, -, _, and space.', + } +); + +export const secretTokenHint = i18n.translate( + 'xpack.apm.onboarding.shared_clients.configure.commands.secretTokenHint', + { + defaultMessage: + 'Use if APM Server requires a secret token. Both the agent and APM Server must be configured with the same token. This ensures that only your agents can send data to your APM server.', + } +); + +export const apiKeyHint = i18n.translate( + 'xpack.apm.onboarding.shared_clients.configure.commands.apiKeyHint', + { + defaultMessage: + 'Use if APM Server requires an API Key. This is used to ensure that only your agents can send data to your APM server. Agents can use API keys as a replacement of secret token, APM server can have multiple API keys. When both secret token and API key are used, API key has priority and secret token is ignored.', + } +); +export const serverUrlHint = i18n.translate( + 'xpack.apm.onboarding.shared_clients.configure.commands.serverUrlHint', + { + defaultMessage: + 'Set the custom APM Server URL (default: {defaultApmServerUrl}). The URL must be fully qualified, including protocol (http or https) and port.', + values: { defaultApmServerUrl: 'http://localhost:8200' }, + } +); + +export const serviceEnvironmentHint = i18n.translate( + 'xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint', + { + defaultMessage: `The name of the environment this service is deployed in, e.g., "production" or "staging". Environments allow you to easily filter data on a global level in the APM UI. It's important to be consistent when naming environments across agents.`, + } +); diff --git a/x-pack/plugins/apm/public/components/app/onboarding/footer.tsx b/x-pack/plugins/apm/public/components/app/onboarding/footer.tsx new file mode 100644 index 0000000000000..0886e5b5ab008 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/footer.tsx @@ -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 { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiText, +} from '@elastic/eui'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useKibanaUrl } from '../../../hooks/use_kibana_url'; + +export function Footer() { + const apmLink = useKibanaUrl('/app/apm'); + return ( + + + + +

+ +

+
+
+ + + + {i18n.translate('xpack.apm.onboarding.footer.cta', { + defaultMessage: 'Launch APM', + })} + + +
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/onboarding/index.tsx b/x-pack/plugins/apm/public/components/app/onboarding/index.tsx new file mode 100644 index 0000000000000..30c065e10b349 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/onboarding/index.tsx @@ -0,0 +1,107 @@ +/* + * 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 React, { useCallback, useEffect, useState } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { EuiSpacer } from '@elastic/eui'; +import { callApmApi } from '../../../services/rest/create_call_apm_api'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { ApmPluginStartDeps } from '../../../plugin'; +import { Introduction } from './introduction'; +import { InstructionsSet } from './instructions_set'; +import { serverlessInstructions } from './serverless_instructions'; +import { Footer } from './footer'; +import { PrivilegeType } from '../../../../common/privilege_type'; +import { AgentApiKey, InstructionSet } from './instruction_variants'; + +export function Onboarding() { + const [instructions, setInstructions] = useState([]); + const [agentApiKey, setAgentApiKey] = useState({ + apiKey: null, + error: false, + }); + const [apiKeyLoading, setApiKeyLoading] = useState(false); + const { services } = useKibana(); + const { config } = useApmPluginContext(); + const { docLinks, observabilityShared } = services; + const guideLink = + docLinks?.links.kibana.guide || + 'https://www.elastic.co/guide/en/kibana/current/index.html'; + + const baseUrl = docLinks?.ELASTIC_WEBSITE_URL || 'https://www.elastic.co/'; + + const createAgentKey = useCallback(async () => { + try { + setApiKeyLoading(true); + const privileges: PrivilegeType[] = [PrivilegeType.EVENT]; + + const { agentKey } = await callApmApi( + 'POST /api/apm/agent_keys 2023-05-22', + { + signal: null, + params: { + body: { + name: `onboarding-${(Math.random() + 1) + .toString(36) + .substring(7)}`, + privileges, + }, + }, + } + ); + + setAgentApiKey({ + apiKey: agentKey.api_key, + encodedKey: agentKey.encoded, + id: agentKey.id, + error: false, + }); + } catch (error) { + setAgentApiKey({ + apiKey: null, + error: true, + errorMessage: error.body?.message || error.message, + }); + } finally { + setApiKeyLoading(false); + } + }, []); + + const instructionsExists = instructions.length > 0; + + useEffect(() => { + // Here setInstructions will be called based on the condition for serverless, cloud or onPrem + // right now we will only call the ServerlessInstruction directly + setInstructions([ + serverlessInstructions( + { + baseUrl, + config, + }, + apiKeyLoading, + agentApiKey, + createAgentKey + ), + ]); + }, [agentApiKey, baseUrl, config, createAgentKey, apiKeyLoading]); + + const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; + return ( + + + + {instructionsExists && + instructions.map((instruction) => ( +
+ + +
+ ))} +