[APM] Serverless Onboarding with Custom Tutorials (#158228)

Closes https://github.com/elastic/kibana/issues/155371
## Summary

PR adds Serverless Onboarding flow using Custom Integration. This would
also lay the foundation for us to complete get rid of Home Tutorial App
and move the remaining `onPrem` and `cloud` tutorials which are
currently still loaded using Home Tutorial App.

1. Adds new Custom Integration for Serverless Onboarding (Toggling Home
AApp Tutorial Integration)
2. Since we are migrating away from the Home App Tutorials, lot of
existing code has been duplicated and refactored for the custom
implementation. Home App Tutorial would require the Server to register
all the steps and the client to only register a custom component which
then would be loaded by Home App Tutorial component. We don't need to
follow this approach any more. All the UX logic has now been moved to
the Public folder with only Custom Integration done on the
`server/plugin.ts`.
3. As we are not sure how the solutions will be informed about being
running on Serverless or not, I have introduced a new variable in
`serverless.oblt.yml` file called `xpack.apm.serverlessOnboarding:
true`. With this the development has been done. This can be changed to
actual logic once we know more.

4. A new configuration `xpack.apm.managedServiceUrl` for accessing
Managed Service URL is also being added by Control Plane team as part of
https://elasticco.atlassian.net/browse/CP-2403. Hence this PR expects
this property to be present for Serverless.

5. Unit tests to toggle between `secret_token` and `api_key` depending
on availability has been added. No API Tests were added as no new API
created. Cypress Tests cannot be added due to Serverless

## Need help reviewing the PR ?

1. `config/serverless.oblt.yml` - Adds the new flag which would enable
this flow
2. `x-pack/plugins/apm/common/tutorial/tutorials.ts` - Defines the
configuration required to register the APM's Tutorial Custom Integration
3. `x-pack/plugins/apm/public/components/app/tutorials/commands` - This
directory contains all the agent specific data required to load the
TABLE with settings required for configuring APM MIS.
4. `x-pack/plugins/apm/public/components/app/tutorials/instructions` -
This folder contains all the individual agent specific instructions in
the format used by
[EuiSteps](https://eui.elastic.co/#/navigation/steps#complex-steps)
5. `x-pack/plugins/apm/public/components/routing` - Here we register our
custom route
6. Changes on the server side a quite small and they only register the
custom integration.
7.
`x-pack/plugins/apm/public/components/app/tutorials/serverless_instructions.tsx`
- This file currently defines all the logic for registering Serverless
instructions. We will soon have similar files for `onPrem` and `cloud`
instructions

### Risk Matrix


| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| The flow depends on presence of a flag in `kibana.yml` file. | Low |
High | By default this flow will be disabled and would fallback to
traditional onboarding in absence of the flag. |

### Demo



d60f0610-1fea-4540-86f5-2d72ab97f640

### Updated Demo with Create API Button inside the table


e84d8d6c-a048-4638-9b63-45080feca90b

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Achyut Jhunjhunwala 2023-06-07 16:20:50 +02:00 committed by GitHub
parent 177914bcaf
commit ac2fc4c3be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 3637 additions and 34 deletions

View file

@ -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

View file

@ -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)',

View file

@ -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<CustomIntegration, 'type'> = {
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,
};

View file

@ -45,6 +45,7 @@
"spaces",
"taskManager",
"usageCollection",
"customIntegrations", // Move this to requiredPlugins after completely migrating from the Tutorials Home App
"licenseManagement"
],
"requiredBundles": [

View file

@ -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 (
<>
<EuiSpacer />
<AgentConfigurationTable
variables={variables}
data={{ apmServerUrl, secretToken, apiKey }}
createApiKey={createApiKey}
createApiKeyLoading={createApiKeyLoading}
/>
<EuiSpacer />
<EuiCodeBlock
isCopyable
language={highlightLang || 'bash'}
data-test-subj="commands"
lineNumbers={lineNumbers}
whiteSpace="pre"
>
{commands}
</EuiCodeBlock>
</>
);
}

View file

@ -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 (
<EuiButton
data-test-subj="createApiKeyAndId"
fill
onClick={createApiKey}
isLoading={createApiKeyLoading}
>
{i18n.translate('xpack.apm.tutorial.apiKey.create', {
defaultMessage: 'Create API Key',
})}
</EuiButton>
);
}
return (
<EuiText size="s" color="accent">
{value}
</EuiText>
);
}
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<EuiBasicTableColumn<ValuesType<typeof items>>> = [
{
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 }) => (
<ConfigurationValueColumn
columnKey={key}
value={value}
createApiKey={createApiKey}
createApiKeyLoading={createApiKeyLoading}
/>
),
},
];
const items = Object.entries(variables).map(([key, value]) => ({
setting: value,
value: get({ ...data, ...defaultValues }, key),
key,
}));
return <EuiBasicTable items={items} columns={columns} />;
}

View file

@ -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',
#...
)`;

View file

@ -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",
}
}`;

View file

@ -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)`;

View file

@ -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-<version>.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-<version>.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-<version>.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:&#x2F;&#x2F;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:&#x2F;&#x2F;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:&#x2F;&#x2F;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\\""
`);
});
});
});

View file

@ -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<string, string> = {
java,
node,
django,
flask,
rails,
rack,
go,
dotnet,
php,
};
interface Variables {
[key: string]: string;
}
const apmAgentVariablesMap: (
secretToken?: string
) => Record<string, Variables> = (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<string, LineNumbers> = (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<string, string> = {
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];
}

View file

@ -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
`;

View file

@ -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-<version>.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`;

View file

@ -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'
})`;

View file

@ -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"`;

View file

@ -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'`;

View file

@ -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'`;

View file

@ -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.`,
}
);

View file

@ -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 (
<EuiPanel paddingSize="l">
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText>
<p>
<FormattedMessage
id="xpack.apm.onboarding.footer.exploreYourDataDescription"
defaultMessage="When all steps are complete, you're ready to explore your data."
/>
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="apmTutorialFooterButton"
fill
href={apmLink}
>
{i18n.translate('xpack.apm.onboarding.footer.cta', {
defaultMessage: 'Launch APM',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -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<InstructionSet[]>([]);
const [agentApiKey, setAgentApiKey] = useState<AgentApiKey>({
apiKey: null,
error: false,
});
const [apiKeyLoading, setApiKeyLoading] = useState(false);
const { services } = useKibana<ApmPluginStartDeps>();
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 (
<ObservabilityPageTemplate>
<Introduction isBeta={false} guideLink={guideLink} />
<EuiSpacer />
{instructionsExists &&
instructions.map((instruction) => (
<div key={instruction.title}>
<InstructionsSet instructions={instruction} />
<EuiSpacer />
</div>
))}
<Footer />
</ObservabilityPageTemplate>
);
}

View file

@ -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 { EuiStepProps } from '@elastic/eui/src/components/steps/step';
export enum INSTRUCTION_VARIANT {
NODE = 'node',
DJANGO = 'django',
FLASK = 'flask',
RAILS = 'rails',
RACK = 'rack',
GO = 'go',
JAVA = 'java',
DOTNET = 'dotnet',
PHP = 'php',
OPEN_TELEMETRY = 'openTelemetry',
}
export interface InstructionVariant {
id: INSTRUCTION_VARIANT;
instructions: EuiStepProps[];
}
export interface InstructionSet {
title: string;
instructionVariants: InstructionVariant[];
}
const DISPLAY_MAP = {
[INSTRUCTION_VARIANT.NODE]: 'Node.js',
[INSTRUCTION_VARIANT.DJANGO]: 'Django',
[INSTRUCTION_VARIANT.FLASK]: 'Flask',
[INSTRUCTION_VARIANT.RAILS]: 'Ruby on Rails',
[INSTRUCTION_VARIANT.RACK]: 'Rack',
[INSTRUCTION_VARIANT.GO]: 'Go',
[INSTRUCTION_VARIANT.JAVA]: 'Java',
[INSTRUCTION_VARIANT.DOTNET]: '.NET',
[INSTRUCTION_VARIANT.PHP]: 'PHP',
[INSTRUCTION_VARIANT.OPEN_TELEMETRY]: 'OpenTelemetry',
};
export function getDisplayText(id: INSTRUCTION_VARIANT) {
return id in DISPLAY_MAP ? DISPLAY_MAP[id] : id;
}
export interface AgentApiKey {
apiKey: string | null;
id?: string;
encodedKey?: string;
error: boolean;
errorMessage?: string;
}
export type AgentApiDetails = AgentApiKey & {
displayApiKeySuccessCallout: boolean;
displayApiKeyErrorCallout: boolean;
createAgentKey: () => void;
createApiKeyLoading: boolean;
};
export interface AgentInstructions {
baseUrl: string;
apmServerUrl: string;
apiKeyDetails?: AgentApiDetails;
secretToken?: string;
}

View file

@ -0,0 +1,88 @@
/*
* 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 { EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
export function ApiKeyCallout({
isSuccess,
isError,
errorMessage,
}: {
isSuccess: boolean;
isError: boolean;
errorMessage?: string;
}) {
if (isSuccess) {
return (
<>
<EuiCallOut
title={i18n.translate(
'xpack.apm.onboarding.apiKey.success.calloutTitle',
{
defaultMessage: 'API key created',
}
)}
color="success"
iconType="check"
>
{i18n.translate(
'xpack.apm.onboarding.apiKey.success.calloutMessage',
{
defaultMessage: `Remember to store this information in a safe place. It won't be displayed anymore after you continue`,
}
)}
</EuiCallOut>
</>
);
}
const regex = /missing the following requested privilege\(s\)/;
const isInsufficientPermissionsError =
isError && regex.test(errorMessage || '');
if (isInsufficientPermissionsError) {
return (
<EuiCallOut
title={i18n.translate(
'xpack.apm.onboarding.apiKey.warning.calloutTitle',
{
defaultMessage: 'User does not have permissions to create API Key',
}
)}
color="warning"
iconType="warning"
>
{i18n.translate('xpack.apm.onboarding.apiKey.warning.calloutMessage', {
defaultMessage:
'User is missing the following privilege - {missingPrivilege}. Please add the missing APM application privilege to the role of the authenticated user',
values: {
missingPrivilege: 'event:write',
},
})}
</EuiCallOut>
);
}
return (
<EuiCallOut
title={i18n.translate('xpack.apm.onboarding.apiKey.error.calloutTitle', {
defaultMessage: 'Failed to create API key',
})}
color="danger"
iconType="error"
>
{i18n.translate('xpack.apm.onboarding.apiKey.error.calloutMessage', {
defaultMessage: 'Error: {errorMessage}',
values: {
errorMessage,
},
})}
</EuiCallOut>
);
}

View file

@ -0,0 +1,90 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createDjangoAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.django.install.title', {
defaultMessage: 'Install the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.django.install.textPre', {
defaultMessage:
'Install the APM agent for Python as a dependency.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
$ pip install elastic-apm
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.django.configure.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.django.configure.textPre', {
defaultMessage:
'Agents are libraries that run inside of your application process. \
APM services are created programmatically based on the `SERVICE_NAME`.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.DJANGO}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.django.configure.textPost', {
defaultMessage:
'See the [documentation]({documentationLink}) for advanced usage.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/python/current/django-support.html`,
},
})}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,151 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createDotNetAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
const codeBlock = `public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAllElasticApm(Configuration);
//…rest of the method
}
//…rest of the class
}`;
return [
{
title: i18n.translate('xpack.apm.onboarding.dotNet.download.title', {
defaultMessage: 'Download the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.dotNet.download.textPre', {
defaultMessage:
'Add the the agent package(s) from [NuGet]({allNuGetPackagesLink}) to your .NET application. There are multiple \
NuGet packages available for different use cases. \n\nFor an ASP.NET Core application with Entity Framework \
Core download the [Elastic.Apm.NetCoreAll]({netCoreAllApmPackageLink}) package. This package will automatically add every \
agent component to your application. \n\n In case you would like to minimize the dependencies, you can use the \
[Elastic.Apm.AspNetCore]({aspNetCorePackageLink}) package for just \
ASP.NET Core monitoring or the [Elastic.Apm.EfCore]({efCorePackageLink}) package for just Entity Framework Core monitoring. \n\n In \
case you only want to use the public Agent API for manual instrumentation use the [Elastic.Apm]({elasticApmPackageLink}) package.',
values: {
allNuGetPackagesLink:
'https://www.nuget.org/packages?q=Elastic.apm',
netCoreAllApmPackageLink:
'https://www.nuget.org/packages/Elastic.Apm.NetCoreAll',
aspNetCorePackageLink:
'https://www.nuget.org/packages/Elastic.Apm.AspNetCore',
efCorePackageLink:
'https://www.nuget.org/packages/Elastic.Apm.EntityFrameworkCore',
elasticApmPackageLink:
'https://www.nuget.org/packages/Elastic.Apm',
},
})}
</EuiMarkdownFormat>
</>
),
},
{
title: i18n.translate(
'xpack.apm.onboarding.dotNet.configureApplication.title',
{
defaultMessage: 'Add the agent to the application',
}
),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.dotNet.configureApplication.textPre',
{
defaultMessage:
'In case of ASP.NET Core with the `Elastic.Apm.NetCoreAll` package, call the `UseAllElasticApm` \
method in the `Configure` method within the `Startup.cs` file.',
}
)}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
{codeBlock}
</EuiCodeBlock>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.dotNet.configureApplication.textPost',
{
defaultMessage:
'Passing an `IConfiguration` instance is optional and by doing so, the agent will read config settings through this \
`IConfiguration` instance (e.g. from the `appsettings.json` file).',
}
)}
</EuiMarkdownFormat>
</>
),
},
{
title: i18n.translate(
'xpack.apm.onboarding.dotNet.configureAgent.title',
{
defaultMessage: 'Sample appsettings.json file:',
}
),
children: (
<>
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.DOTNET}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.dotNet.configureAgent.textPost',
{
defaultMessage:
'In case you dont pass an `IConfiguration` instance to the agent (e.g. in case of non ASP.NET Core applications) \
you can also configure the agent through environment variables. \n \
See [the documentation]({documentationLink}) for advanced usage, including the [Profiler Auto instrumentation]({profilerLink}) quick start.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/dotnet/current/configuration.html`,
profilerLink: `${baseUrl}guide/en/apm/agent/dotnet/current/setup-auto-instrumentation.html#setup-auto-instrumentation`,
},
}
)}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,90 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createFlaskAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.flask.install.title', {
defaultMessage: 'Install the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.flask.install.textPre', {
defaultMessage:
'Install the APM agent for Python as a dependency.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
$ pip install elastic-apm[flask]
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.flask.configure.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.flask.configure.textPre', {
defaultMessage:
'Agents are libraries that run inside of your application process. \
APM services are created programmatically based on the `SERVICE_NAME`.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.FLASK}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.flask.configure.textPost', {
defaultMessage:
'See the [documentation]({documentationLink}) for advanced usage.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/python/current/flask-support.html`,
},
})}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,134 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createGoAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
const codeBlock = `\
import (
"net/http"
"go.elastic.co/apm/module/apmhttp"
)
func main() {
mux := http.NewServeMux()
...
http.ListenAndServe(":8080", apmhttp.Wrap(mux))
}
`;
return [
{
title: i18n.translate('xpack.apm.onboarding.go.install.title', {
defaultMessage: 'Install the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.go.install.textPre', {
defaultMessage: 'Install the APM agent packages for Go.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
go get go.elastic.co/apm
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.go.configure.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.go.configure.textPre', {
defaultMessage:
'Agents are libraries that run inside of your application process. \
APM services are created programmatically based on the executable \
file name, or the `ELASTIC_APM_SERVICE_NAME` environment variable.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.GO}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.go.configure.textPost', {
defaultMessage:
'See the [documentation]({documentationLink}) for advanced configuration.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/go/current/configuration.html`,
},
})}
</EuiMarkdownFormat>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.go.goClient.title', {
defaultMessage: 'Instrument your application',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.go.instrument.textPre', {
defaultMessage:
'Instrument your Go application by using one of the provided instrumentation modules or \
by using the tracer API directly.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="go" isCopyable={true}>
{codeBlock}
</EuiCodeBlock>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.go.instrument.textPost', {
defaultMessage:
'See the [documentation]({documentationLink}) for a detailed \
guide to instrumenting Go source code.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/go/current/instrumenting-source.html`,
},
})}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,17 @@
/*
* 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 { createJavaAgentInstructions } from './java_agent';
export { createNodeAgentInstructions } from './node_agent';
export { createDjangoAgentInstructions } from './django_agent';
export { createFlaskAgentInstructions } from './flask_agent';
export { createRailsAgentInstructions } from './rails_agent';
export { createRackAgentInstructions } from './rack_agent';
export { createGoAgentInstructions } from './go_agent';
export { createDotNetAgentInstructions } from './dotnet_agent';
export { createPhpAgentInstructions } from './php_agent';
export { createOpenTelemetryAgentInstructions } from './otel_agent';

View file

@ -0,0 +1,104 @@
/*
* 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 { EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createJavaAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.java.download.title', {
defaultMessage: 'Download the APM agent',
}),
children: (
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.java.download.textPre', {
defaultMessage:
'Download the agent jar from [Maven Central]({mavenCentralLink}). \
Do **not** add the agent as a dependency to your application.',
values: {
mavenCentralLink:
'https://oss.sonatype.org/service/local/artifact/maven/redirect?r=releases&g=co.elastic.apm&a=elastic-apm-agent&v=LATEST',
},
})}
</EuiMarkdownFormat>
),
},
{
title: i18n.translate(
'xpack.apm.onboarding.java.startApplication.title',
{
defaultMessage: 'Start your application with the javaagent flag',
}
),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.java.startApplication.textPre',
{
defaultMessage:
'Add the `-javaagent` flag and configure the agent with system properties.\n\n \
* Set the required service name (allowed characters: a-z, A-Z, 0-9, -, _, and space)\n \
* Set the custom APM Server URL (default: {customApmServerUrl})\n \
* Set the APM Server secret token\n \
* Set the service environment\n \
* Set the base package of your application',
values: { customApmServerUrl: 'http://localhost:8200' },
}
)}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.JAVA}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.java.startApplication.textPost',
{
defaultMessage:
'See the [documentation]({documentationLink}) for configuration options and advanced \
usage.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/java/current/index.html`,
},
}
)}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,93 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { ApiKeyCallout } from './api_key_callout';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
export const createNodeAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.node.install.title', {
defaultMessage: 'Install the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.node.install.textPre', {
defaultMessage:
'Install the APM agent for Node.js as a dependency to your application.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
npm install elastic-apm-node --save
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.node.configure.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.node.configure.textPre', {
defaultMessage:
'Agents are libraries that run inside of your application process. \
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.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.NODE}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.node.configure.textPost', {
defaultMessage:
'See [the documentation]({documentationLink}) for advanced usage, including how to use with \
[Babel/ES Modules]({babelEsModulesLink}).',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/nodejs/current/index.html`,
babelEsModulesLink: `${baseUrl}guide/en/apm/agent/nodejs/current/advanced-setup.html#es-modules`,
},
})}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,290 @@
/*
* 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 {
EuiBasicTable,
EuiBasicTableColumn,
EuiButton,
EuiLink,
EuiMarkdownFormat,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { ValuesType } from 'utility-types';
import { FormattedMessage } from '@kbn/i18n-react';
import { AgentApiDetails, AgentInstructions } from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createOpenTelemetryAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.otel.download.title', {
defaultMessage: 'Download the OpenTelemetry APM Agent or SDK',
}),
children: (
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.otel.download.textPre', {
defaultMessage:
'See the [OpenTelemetry Instrumentation guides]({openTelemetryInstrumentationLink}) to download the OpenTelemetry Agent or SDK for your language.',
values: {
openTelemetryInstrumentationLink:
'https://opentelemetry.io/docs/instrumentation',
},
})}
</EuiMarkdownFormat>
),
},
{
title: i18n.translate('xpack.apm.onboarding.otel.configureAgent.title', {
defaultMessage: 'Configure OpenTelemetry in your application',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.otel.configureAgent.textPre',
{
defaultMessage:
'Specify the following OpenTelemetry settings as part of the startup of your application. Note that OpenTelemetry SDKs require some bootstrap code in addition to these configuration settings. For more details, see the [Elastic OpenTelemetry documentation]({openTelemetryDocumentationLink}) and the [OpenTelemetry community instrumentation guides]({openTelemetryInstrumentationLink}).',
values: {
openTelemetryDocumentationLink: `${baseUrl}guide/en/apm/guide/current/open-telemetry.html`,
openTelemetryInstrumentationLink:
'https://opentelemetry.io/docs/instrumentation',
},
}
)}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<OpenTelemetryInstructions
apmServerUrl={apmServerUrl}
apiKeyDetails={apiKeyDetails}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.otel.configureAgent.textPost',
{
defaultMessage:
'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n',
values: {
documentationLink: `${baseUrl}guide/en/apm/guide/current/open-telemetry.html`,
},
}
)}
</EuiMarkdownFormat>
</>
),
},
];
};
function ConfigurationValueColumn({
setting,
value,
createApiKey,
createApiKeyLoading,
apiKey,
}: {
setting: string;
value: string;
createApiKey?: () => void;
createApiKeyLoading?: boolean;
apiKey?: string | null;
}) {
const shouldRenderCreateApiKeyButton =
setting === 'OTEL_EXPORTER_OTLP_HEADERS' && apiKey === null;
if (shouldRenderCreateApiKeyButton) {
return (
<EuiButton
data-test-subj="createApiKeyAndId"
fill
onClick={createApiKey}
isLoading={createApiKeyLoading}
>
{i18n.translate('xpack.apm.onboarding.apiKey.create', {
defaultMessage: 'Create API Key',
})}
</EuiButton>
);
}
return (
<EuiText size="s" color="accent">
{value}
</EuiText>
);
}
export function OpenTelemetryInstructions({
apmServerUrl,
secretToken,
apiKeyDetails,
}: {
apmServerUrl: string;
secretToken?: string;
apiKeyDetails?: AgentApiDetails;
}) {
let authHeaderValue;
if (secretToken) {
authHeaderValue = `Authorization=Bearer ${secretToken}`;
} else {
authHeaderValue = `Authorization=ApiKey ${apiKeyDetails?.encodedKey}`;
}
const items = [
{
setting: 'OTEL_EXPORTER_OTLP_ENDPOINT',
value: apmServerUrl ? apmServerUrl : '<apm-server-url>',
},
{
setting: 'OTEL_EXPORTER_OTLP_HEADERS',
value: authHeaderValue,
apiKey: apiKeyDetails?.apiKey,
},
{
setting: 'OTEL_METRICS_EXPORTER',
value: 'otlp',
notes: 'Enable metrics when supported by your OpenTelemetry client.',
},
{
setting: 'OTEL_LOGS_EXPORTER',
value: 'otlp',
notes: 'Enable logs when supported by your OpenTelemetry client',
},
{
setting: 'OTEL_RESOURCE_ATTRIBUTES',
value:
'service.name=<app-name>,service.version=<app-version>,deployment.environment=production',
},
];
const columns: Array<EuiBasicTableColumn<ValuesType<typeof items>>> = [
{
field: 'setting',
name: i18n.translate(
'xpack.apm.onboarding.config_otel.column.configSettings',
{
defaultMessage: 'Configuration setting (1)',
}
),
},
{
field: 'value',
name: i18n.translate(
'xpack.apm.onboarding.config_otel.column.configValue',
{
defaultMessage: 'Configuration value',
}
),
render: (_, { value, setting, apiKey }) => (
<ConfigurationValueColumn
setting={setting}
value={value}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
apiKey={apiKeyDetails?.apiKey}
/>
),
},
{
field: 'notes',
name: i18n.translate('xpack.apm.onboarding.config_otel.column.notes', {
defaultMessage: 'Notes',
}),
},
];
return (
<>
<EuiBasicTable
items={items}
columns={columns}
data-test-subj="otel-instructions-table"
/>
<EuiSpacer size="m" />
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.apm.onboarding.config_otel.description1"
defaultMessage="(1) OpenTelemetry agents and SDKs must support the {otelExporterOtlpEndpoint}, {otelExporterOtlpHeaders}, and {otelResourceAttributes} variables; some unstable components may not yet comply with this requirement."
values={{
otelExporterOtlpEndpoint: (
<EuiLink
data-test-subj="apmOpenTelemetryInstructionsOtelExporterOtlpEndpointLink"
target="_blank"
href="https://github.com/open-telemetry/opentelemetry-specification/blob/v1.10.0/specification/protocol/exporter.md"
>
OTEL_EXPORTER_OTLP_ENDPOINT
</EuiLink>
),
otelExporterOtlpHeaders: (
<EuiLink
data-test-subj="apmOpenTelemetryInstructionsOtelExporterOtlpHeadersLink"
target="_blank"
href="https://github.com/open-telemetry/opentelemetry-specification/blob/v1.10.0/specification/protocol/exporter.md"
>
OTEL_EXPORTER_OTLP_HEADERS
</EuiLink>
),
otelResourceAttributes: (
<EuiLink
data-test-subj="apmOpenTelemetryInstructionsOtelResourceAttributesLink"
target="_blank"
href="https://github.com/open-telemetry/opentelemetry-specification/blob/v1.10.0/specification/resource/sdk.md"
>
OTEL_RESOURCE_ATTRIBUTES
</EuiLink>
),
}}
/>
<EuiSpacer size="xs" />
<FormattedMessage
id="xpack.apm.onboarding.config_otel.description2"
defaultMessage="The 'OTEL_METRICS_EXPORTER` and 'OTEL_LOGS_EXPORTER' environment variables may not be supported by some SDKs."
/>
<EuiSpacer size="xs" />
<FormattedMessage
id="xpack.apm.onboarding.config_otel.description3"
defaultMessage="The exhaustive list of environment variables, command line parameters, and configuration code snippets (according to the OpenTelemetry specification) is available in the {otelInstrumentationGuide}. Some unstable OpenTelemetry clients may not support all features and may require alternate configuration mechanisms."
values={{
otelInstrumentationGuide: (
<EuiLink
data-test-subj="apmOpenTelemetryInstructionsOpenTelemetryInstrumentationGuideLink"
target="_blank"
href="https://opentelemetry.io/docs/instrumentation"
>
{i18n.translate(
'xpack.apm.onboarding.config_otel.instrumentationGuide',
{
defaultMessage: 'OpenTelemetry Instrumentation guide',
}
)}
</EuiLink>
),
}}
/>
</EuiText>
</>
);
}

View file

@ -0,0 +1,123 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createPhpAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.php.download.title', {
defaultMessage: 'Download the APM agent',
}),
children: (
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.php.download.textPre', {
defaultMessage:
'Download the package corresponding to your platform from [GitHub releases]({githubReleasesLink}).',
values: {
githubReleasesLink:
'https://github.com/elastic/apm-agent-php/releases',
},
})}
</EuiMarkdownFormat>
),
},
{
title: i18n.translate('xpack.apm.onboarding.php.installPackage.title', {
defaultMessage: 'Install the downloaded package',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.php.installPackage.textPre', {
defaultMessage: 'For example on Alpine Linux using APK package:',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
apk add --allow-untrusted &lt;package-file&gt;.apk
</EuiCodeBlock>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.php.installPackage.textPost',
{
defaultMessage:
'See the [documentation]({documentationLink}) for installation commands on other supported platforms and advanced installation.',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/php/current/setup.html`,
},
}
)}
</EuiMarkdownFormat>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.php.configureAgent.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.php.Configure the agent.textPre',
{
defaultMessage:
'APM is automatically started when your app boots. Configure the agent either via `php.ini` file:',
}
)}
</EuiMarkdownFormat>
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.PHP}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate(
'xpack.apm.onboarding.php.configureAgent.textPost',
{
defaultMessage:
'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/php/current/configuration.html`,
},
}
)}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,134 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createRackAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
const codeBlock = `# config.ru
require 'sinatra/base'
class MySinatraApp < Sinatra::Base
use ElasticAPM::Middleware
# ...
end
ElasticAPM.start(
app: MySinatraApp, # ${i18n.translate(
'xpack.apm.onboarding.rack.configure.commands.requiredComment',
{
defaultMessage: 'required',
}
)}
config_file: '' # ${i18n.translate(
'xpack.apm.onboarding.rack.configure.commands.optionalComment',
{
defaultMessage: 'optional, defaults to config/elastic_apm.yml',
}
)}
)
run MySinatraApp
at_exit { ElasticAPM.stop }`;
return [
{
title: i18n.translate('xpack.apm.onboarding.rack.install.title', {
defaultMessage: 'Install the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rack.install.textPre', {
defaultMessage: 'Add the agent to your Gemfile.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
gem &apos;elastic-apm&apos;
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.rack.configure.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rack.configure.textPre', {
defaultMessage:
'For Rack or a compatible framework (e.g. Sinatra), include the middleware in your app and start the agent.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
{codeBlock}
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.rack.createConfig.title', {
defaultMessage: 'Create config file',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rack.createConfig.textPre', {
defaultMessage: 'Create a config file {configFile}:',
values: { configFile: '`config/elastic_apm.yml`' },
})}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.RACK}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rack.configure.textPost', {
defaultMessage:
'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/ruby/current/index.html`,
},
})}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,89 @@
/*
* 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 { EuiCodeBlock, EuiMarkdownFormat, EuiSpacer } from '@elastic/eui';
import { EuiStepProps } from '@elastic/eui/src/components/steps/step';
import React from 'react';
import { AgentConfigInstructions } from '../agent_config_instructions';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
} from '../instruction_variants';
import { ApiKeyCallout } from './api_key_callout';
export const createRailsAgentInstructions = (
commonOptions: AgentInstructions
): EuiStepProps[] => {
const { baseUrl, apmServerUrl, apiKeyDetails } = commonOptions;
return [
{
title: i18n.translate('xpack.apm.onboarding.rails.install.title', {
defaultMessage: 'Install the APM agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rails.install.textPre', {
defaultMessage: 'Add the agent to your Gemfile.',
})}
</EuiMarkdownFormat>
<EuiSpacer />
<EuiCodeBlock language="bash" isCopyable={true}>
gem &apos;elastic-apm&apos;
</EuiCodeBlock>
</>
),
},
{
title: i18n.translate('xpack.apm.onboarding.rails.configure.title', {
defaultMessage: 'Configure the agent',
}),
children: (
<>
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rails.configure.textPre', {
defaultMessage:
'APM is automatically started when your app boots. Configure the agent, by creating the config file {configFile}',
values: { configFile: '`config/elastic_apm.yml`' },
})}
</EuiMarkdownFormat>
<EuiSpacer />
{(apiKeyDetails?.displayApiKeySuccessCallout ||
apiKeyDetails?.displayApiKeyErrorCallout) && (
<>
<ApiKeyCallout
isError={apiKeyDetails?.displayApiKeyErrorCallout}
isSuccess={apiKeyDetails?.displayApiKeySuccessCallout}
errorMessage={apiKeyDetails?.errorMessage}
/>
<EuiSpacer />
</>
)}
<AgentConfigInstructions
variantId={INSTRUCTION_VARIANT.RAILS}
apmServerUrl={apmServerUrl}
apiKey={apiKeyDetails?.apiKey}
createApiKey={apiKeyDetails?.createAgentKey}
createApiKeyLoading={apiKeyDetails?.createApiKeyLoading}
/>
<EuiSpacer />
<EuiMarkdownFormat>
{i18n.translate('xpack.apm.onboarding.rails.configure.textPost', {
defaultMessage:
'See the [documentation]({documentationLink}) for configuration options and advanced usage.\n\n',
values: {
documentationLink: `${baseUrl}guide/en/apm/agent/ruby/current/index.html`,
},
})}
</EuiMarkdownFormat>
</>
),
},
];
};

View file

@ -0,0 +1,101 @@
/*
* 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 {
EuiSplitPanel,
EuiTabs,
EuiTab,
EuiTitle,
EuiSteps,
EuiSpacer,
} from '@elastic/eui';
import React, { useState } from 'react';
import {
INSTRUCTION_VARIANT,
getDisplayText,
InstructionVariant,
InstructionSet,
} from './instruction_variants';
interface AgentTab {
id: INSTRUCTION_VARIANT;
text: string;
}
function getTabs(variants: InstructionVariant[]): AgentTab[] {
return variants.map((variant) => ({
id: variant.id,
text: getDisplayText(variant.id),
}));
}
export function InstructionsSet({
instructions,
}: {
instructions: InstructionSet;
}) {
const tabs = getTabs(instructions.instructionVariants);
const [selectedTab, setSelectedTab] = useState<string>(tabs[0].id);
const onSelectedTabChange = (tab: string) => {
setSelectedTab(tab);
};
function InstructionTabs({ agentTabs }: { agentTabs: AgentTab[] }) {
return (
<EuiTabs>
{agentTabs.map((tab) => (
<EuiTab
key={tab.id}
isSelected={tab.id === selectedTab}
onClick={() => onSelectedTabChange(tab.id)}
>
{tab.text}
</EuiTab>
))}
</EuiTabs>
);
}
function InstructionSteps({
instructionVariants,
tab,
}: {
instructionVariants: InstructionVariant[];
tab: string;
}) {
const selectInstructionSteps = instructionVariants.find((variant) => {
return variant.id === tab;
});
if (!selectInstructionSteps) {
return <></>;
}
return (
<EuiSteps titleSize="xs" steps={selectInstructionSteps.instructions} />
);
}
return (
<EuiSplitPanel.Outer>
<EuiSplitPanel.Inner color="subdued" paddingSize="none">
<InstructionTabs agentTabs={tabs} />
</EuiSplitPanel.Inner>
<EuiSplitPanel.Inner paddingSize="l">
<EuiTitle size="m">
<h2>{instructions.title}</h2>
</EuiTitle>
<EuiSpacer />
<InstructionSteps
instructionVariants={instructions.instructionVariants}
tab={selectedTab}
/>
</EuiSplitPanel.Inner>
</EuiSplitPanel.Outer>
);
}

View file

@ -0,0 +1,85 @@
/*
* 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 {
EuiBetaBadge,
EuiImage,
EuiMarkdownFormat,
EuiPageHeader,
} from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useKibanaUrl } from '../../../hooks/use_kibana_url';
interface IntroductionProps {
isBeta?: boolean;
guideLink: string;
}
export function Introduction({ isBeta, guideLink }: IntroductionProps) {
const betaBadge = (
<EuiBetaBadge
label={i18n.translate('xpack.apm.onboarding.betaLabel', {
defaultMessage: 'Beta',
})}
/>
);
const previewImage = useKibanaUrl('/plugins/apm/assets/apm.png');
const rightSideItems = [
<EuiImage
size="l"
allowFullScreen
fullScreenIconColor="dark"
alt={i18n.translate(
'xpack.apm.onboarding.introduction.imageAltDescription',
{
defaultMessage: 'screenshot of primary dashboard.',
}
)}
url={previewImage}
/>,
];
const description = i18n.translate(
'xpack.apm.onboarding.specProvider.longDescription',
{
defaultMessage:
'Application Performance Monitoring (APM) collects in-depth \
performance metrics and errors from inside your application. \
It allows you to monitor the performance of thousands of applications in real time. \
[Learn more]({learnMoreLink}).',
values: {
learnMoreLink: guideLink,
},
}
);
return (
<>
<EuiPageHeader
iconType="apmApp"
pageTitle={
<>
{i18n.translate('xpack.apm.onboarding.appName', {
defaultMessage: 'APM',
})}
{isBeta && (
<>
&nbsp;
{betaBadge}
</>
)}
</>
}
description={<EuiMarkdownFormat>{description}</EuiMarkdownFormat>}
rightSideItems={rightSideItems}
/>
</>
);
}

View file

@ -0,0 +1,102 @@
/*
* 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 { ConfigSchema } from '../../..';
import {
INSTRUCTION_VARIANT,
AgentInstructions,
AgentApiKey,
} from './instruction_variants';
import {
createJavaAgentInstructions,
createNodeAgentInstructions,
createDjangoAgentInstructions,
createFlaskAgentInstructions,
createRailsAgentInstructions,
createRackAgentInstructions,
createGoAgentInstructions,
createDotNetAgentInstructions,
createPhpAgentInstructions,
createOpenTelemetryAgentInstructions,
} from './instructions';
export function serverlessInstructions(
{
baseUrl,
config,
}: {
baseUrl: string;
config: ConfigSchema;
},
apiKeyLoading: boolean,
apiKeyDetails: AgentApiKey,
createAgentKey: () => void
) {
const { apiKey, error, errorMessage } = apiKeyDetails;
const displayApiKeySuccessCallout = Boolean(apiKey) && !error;
const displayApiKeyErrorCallout = error && Boolean(errorMessage);
const commonOptions: AgentInstructions = {
baseUrl,
apmServerUrl: config.managedServiceUrl,
apiKeyDetails: {
...apiKeyDetails,
displayApiKeySuccessCallout,
displayApiKeyErrorCallout,
createAgentKey,
createApiKeyLoading: apiKeyLoading,
},
};
return {
title: i18n.translate('xpack.apm.tutorial.apmAgents.title', {
defaultMessage: 'APM Agents',
}),
instructionVariants: [
{
id: INSTRUCTION_VARIANT.NODE,
instructions: createNodeAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.DJANGO,
instructions: createDjangoAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.FLASK,
instructions: createFlaskAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.RAILS,
instructions: createRailsAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.RACK,
instructions: createRackAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.GO,
instructions: createGoAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.JAVA,
instructions: createJavaAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.DOTNET,
instructions: createDotNetAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.PHP,
instructions: createPhpAgentInstructions(commonOptions),
},
{
id: INSTRUCTION_VARIANT.OPEN_TELEMETRY,
instructions: createOpenTelemetryAgentInstructions(commonOptions),
},
],
};
}

View file

@ -17,6 +17,7 @@ import { homeRoute } from './home';
import { serviceDetailRoute } from './service_detail';
import { mobileServiceDetailRoute } from './mobile_service_detail';
import { settingsRoute } from './settings';
import { onboarding } from './onboarding';
import { ApmMainTemplate } from './templates/apm_main_template';
import { ServiceGroupsList } from '../app/service_groups';
import { offsetRt } from '../../../common/comparison_rt';
@ -105,6 +106,7 @@ const apmRoutes = {
]),
}),
},
...onboarding,
...diagnosticsRoute,
...settingsRoute,
...serviceDetailRoute,

View file

@ -0,0 +1,15 @@
/*
* 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 { Onboarding } from '../../app/onboarding';
export const onboarding = {
'/onboarding': {
element: <Onboarding />,
},
};

View file

@ -18,6 +18,7 @@ import { ServiceGroupSaveButton } from '../../app/service_groups';
import { ServiceGroupsButtonGroup } from '../../app/service_groups/service_groups_button_group';
import { ApmEnvironmentFilter } from '../../shared/environment_filter';
import { getNoDataConfig } from './no_data_config';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
// Paths that must skip the no data screen
const bypassNoDataScreenPaths = ['/settings', '/diagnostics'];
@ -56,6 +57,7 @@ export function ApmMainTemplate({
const { services } = useKibana<ApmPluginStartDeps>();
const { http, docLinks, observabilityShared, application } = services;
const basePath = http?.basePath.get();
const { config } = useApmPluginContext();
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
@ -101,6 +103,7 @@ export function ApmMainTemplate({
hasApmIntegrations: fleetApmPoliciesData?.hasApmPolicies,
shouldBypassNoDataScreen,
loading: isLoading,
isServerless: config?.serverlessOnboarding,
});
const rightSideItems = [

View file

@ -8,6 +8,56 @@
import { i18n } from '@kbn/i18n';
import type { NoDataConfig } from '@kbn/shared-ux-page-kibana-template';
function getNoDataConfigDetails({
basePath,
isServerless,
hasApmIntegrations,
}: {
basePath?: string;
isServerless?: boolean;
hasApmIntegrations?: boolean;
}) {
const description = i18n.translate(
'xpack.apm.ux.overview.agent.description',
{
defaultMessage:
'Use APM agents to collect APM data. We make it easy with agents for many popular languages.',
}
);
const addDataTitle = i18n.translate(
'xpack.apm.noDataConfig.addDataButtonLabel',
{
defaultMessage: 'Add data',
}
);
if (isServerless) {
return {
title: addDataTitle,
href: `${basePath}/app/apm/onboarding`,
description,
};
}
if (hasApmIntegrations) {
return {
title: addDataTitle,
href: `${basePath}/app/home#/tutorial/apm`,
description,
};
}
return {
title: i18n.translate(
'xpack.apm.noDataConfig.addApmIntegrationButtonLabel',
{ defaultMessage: 'Add the APM integration' }
),
href: `${basePath}/app/integrations/detail/apm/overview`,
description,
};
}
export function getNoDataConfig({
docsLink,
shouldBypassNoDataScreen,
@ -15,6 +65,7 @@ export function getNoDataConfig({
basePath,
hasApmData,
hasApmIntegrations,
isServerless,
}: {
docsLink: string;
shouldBypassNoDataScreen: boolean;
@ -22,27 +73,17 @@ export function getNoDataConfig({
basePath?: string;
hasApmData?: boolean;
hasApmIntegrations?: boolean;
isServerless?: boolean;
}): NoDataConfig | undefined {
// don't show "no data screen" when there is APM data or it should be bypassed
if (hasApmData || shouldBypassNoDataScreen || loading) {
return;
}
const noDataConfigDetails = hasApmIntegrations
? {
title: i18n.translate('xpack.apm.noDataConfig.addDataButtonLabel', {
defaultMessage: 'Add data',
}),
href: `${basePath}/app/home#/tutorial/apm`,
}
: {
title: i18n.translate(
'xpack.apm.noDataConfig.addApmIntegrationButtonLabel',
{
defaultMessage: 'Add the APM integration',
}
),
href: `${basePath}/app/integrations/detail/apm/overview`,
};
const noDataConfigDetails = getNoDataConfigDetails({
basePath,
isServerless,
hasApmIntegrations,
});
return {
solution: i18n.translate('xpack.apm.noDataConfig.solutionName', {
@ -51,10 +92,7 @@ export function getNoDataConfig({
action: {
elasticAgent: {
title: noDataConfigDetails.title,
description: i18n.translate('xpack.apm.ux.overview.agent.description', {
defaultMessage:
'Use APM agents to collect APM data. We make it easy with agents for many popular languages.',
}),
description: noDataConfigDetails.description,
href: noDataConfigDetails.href,
},
},

View file

@ -69,6 +69,8 @@ const mockConfig: ConfigSchema = {
enabled: false,
},
latestAgentVersionsUrl: '',
serverlessOnboarding: false,
managedServiceUrl: '',
};
const urlService = new UrlService({

View file

@ -14,6 +14,8 @@ export interface ConfigSchema {
enabled: boolean;
};
latestAgentVersionsUrl: string;
serverlessOnboarding: boolean;
managedServiceUrl: string;
}
export const plugin: PluginInitializer<ApmPluginSetup, ApmPluginStart> = (

View file

@ -57,6 +57,18 @@ const configSchema = schema.object({
defaultValue: 'https://apm-agent-versions.elastic.co/versions.json',
}),
enabled: schema.boolean({ defaultValue: true }),
serverlessOnboarding: schema.conditional(
schema.contextRef('serverless'),
true,
schema.boolean({ defaultValue: false }),
schema.never()
),
managedServiceUrl: schema.conditional(
schema.contextRef('serverless'),
true,
schema.string({ defaultValue: '' }),
schema.never()
),
});
// plugin config
@ -115,6 +127,8 @@ export const config: PluginConfigDescriptor<APMConfig> = {
serviceMapEnabled: true,
ui: true,
latestAgentVersionsUrl: true,
managedServiceUrl: true,
serverlessOnboarding: true,
},
schema: configSchema,
};

View file

@ -56,6 +56,7 @@ import { scheduleSourceMapMigration } from './routes/source_maps/schedule_source
import { createApmSourceMapIndexTemplate } from './routes/source_maps/create_apm_source_map_index_template';
import { addApiKeysToEveryPackagePolicyIfMissing } from './routes/fleet/api_keys/add_api_keys_to_policies_if_missing';
import { getApmFeatureFlags } from '../common/apm_feature_flags';
import { apmTutorialCustomIntegration } from '../common/tutorial/tutorials';
export class APMPlugin
implements
@ -68,6 +69,7 @@ export class APMPlugin
{
private currentConfig?: APMConfig;
private logger?: Logger;
constructor(private readonly initContext: PluginInitializerContext) {
this.initContext = initContext;
}
@ -148,16 +150,26 @@ export class APMPlugin
});
};
boundGetApmIndices().then((indices) => {
plugins.home?.tutorials.registerTutorial(
tutorialProvider({
apmConfig: currentConfig,
apmIndices: indices,
cloud: plugins.cloud,
isFleetPluginEnabled: !isEmpty(resourcePlugins.fleet),
})
// This if else block will go away in favour of removing Home Tutorial Integration
// Ideally we will directly register a custom integration and pass the configs
// for cloud, onPrem and Serverless so that the actual component can take
// care of rendering
if (currentConfig.serverlessOnboarding && plugins.customIntegrations) {
plugins.customIntegrations?.registerCustomIntegration(
apmTutorialCustomIntegration
);
});
} else {
boundGetApmIndices().then((indices) => {
plugins.home?.tutorials.registerTutorial(
tutorialProvider({
apmConfig: currentConfig,
apmIndices: indices,
cloud: plugins.cloud,
isFleetPluginEnabled: !isEmpty(resourcePlugins.fleet),
})
);
});
}
const telemetryUsageCounter =
resourcePlugins.usageCollection?.setup.createUsageCounter(

View file

@ -68,7 +68,7 @@ export async function createAgentKey({
}],
...
}`;
throw Boom.internal(error);
throw Boom.internal(error, { missingPrivileges }, 403);
}
const body = {

View file

@ -257,7 +257,7 @@ describe('createApi', () => {
expect(response.ok).not.toHaveBeenCalled();
expect(response.custom).toHaveBeenCalledWith({
body: {
attributes: { _inspect: [] },
attributes: { _inspect: [], data: null },
message:
'Invalid value 1 supplied to : Partial<{| query: Partial<{| _inspect: pipe(JSON, boolean) |}> |}>/query: Partial<{| _inspect: pipe(JSON, boolean) |}>/_inspect: pipe(JSON, boolean)',
},

View file

@ -170,6 +170,7 @@ export function registerRoutes({
body: {
message: error.message,
attributes: {
data: {},
_inspect: inspectableEsQueriesMap.get(request),
},
},
@ -181,6 +182,7 @@ export function registerRoutes({
if (Boom.isBoom(error)) {
opts.statusCode = error.output.statusCode;
opts.body.attributes.data = error?.data;
}
// capture error with APM node agent

View file

@ -53,6 +53,10 @@ import {
import { InfraPluginStart, InfraPluginSetup } from '@kbn/infra-plugin/server';
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
import {
CustomIntegrationsPluginSetup,
CustomIntegrationsPluginStart,
} from '@kbn/custom-integrations-plugin/server';
import { APMConfig } from '.';
import { ApmIndicesConfig } from './routes/settings/apm_indices/get_apm_indices';
import { APMEventClient } from './lib/helpers/create_es_client/create_apm_event_client';
@ -90,8 +94,8 @@ export interface APMPluginSetupDependencies {
spaces?: SpacesPluginSetup;
taskManager?: TaskManagerSetupContract;
usageCollection?: UsageCollectionSetup;
customIntegrations?: CustomIntegrationsPluginSetup;
}
export interface APMPluginStartDependencies {
// required dependencies
data: DataPluginStart;
@ -114,4 +118,5 @@ export interface APMPluginStartDependencies {
spaces?: SpacesPluginStart;
taskManager?: TaskManagerStartContract;
usageCollection?: undefined;
customIntegrations?: CustomIntegrationsPluginStart;
}

View file

@ -89,6 +89,7 @@
"@kbn/shared-ux-prompt-not-found",
"@kbn/ui-actions-plugin",
"@kbn/observability-alert-details",
"@kbn/custom-integrations-plugin",
"@kbn/dashboard-plugin",
"@kbn/controls-plugin",
"@kbn/core-http-server",

View file

@ -54,8 +54,14 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const error = await expectToReject<ApmApiError>(() =>
createAgentKey(apmApiClient.writeUser)
);
expect(error.res.status).to.be(500);
expect(error.res.status).to.be(403);
expect(error.res.body.message).contain('is missing the following requested privilege');
expect(error.res.body.attributes).to.eql({
_inspect: [],
data: {
missingPrivileges: allApplicationPrivileges,
},
});
});
it('should return an error when invalidating an agent key', async () => {
@ -79,7 +85,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const error = await expectToReject<ApmApiError>(() =>
createAgentKey(apmApiClient.manageOwnAgentKeysUser, [privilege])
);
expect(error.res.status).to.be(500);
expect(error.res.status).to.be(403);
expect(error.res.body.message).contain('is missing the following requested privilege');
});
});