[APM] Auto attachment for java agent beta in APM integration settings (#119131) (#120691)

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* adds initial discovery rule for `include-vmargs elastic.apm.attach=true`

* adds support for type descriptions

* adding java_attacher_agent_version field

* fixing some stuff

* adding link to doc

* adding internationalization

* updating user description

* fixing default version

* setting to null when disabled

* - fixes encoding and decoding discovery rules yaml
- adds workaround for extra 'elasticsearch' field on integration policy updates
- updates migration package version from 7.16.0 to 8.0.0-dev4

* addressing pr comments

* fixing ci

* fixing elements not visible while dragging

* addressing PR changes

* beta

* adding tooltip back

* addressing pr comments

Co-authored-by: cauemarcondes <caue.marcondes@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Oliver Gupte <ogupte@users.noreply.github.com>
Co-authored-by: cauemarcondes <caue.marcondes@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-07 18:05:00 -05:00 committed by GitHub
parent 04fee5b82b
commit 848705a1f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1853 additions and 76 deletions

View file

@ -8,7 +8,7 @@ import semverParse from 'semver/functions/parse';
export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud';
export const SUPPORTED_APM_PACKAGE_VERSION = '7.16.0';
export const SUPPORTED_APM_PACKAGE_VERSION = '8.0.0-dev4'; // TODO update to just '8.0.0' once published
export function isPrereleaseVersion(version: string) {
return semverParse(version)?.prerelease?.length ?? 0 > 0;

View file

@ -12,19 +12,29 @@ import {
EuiSpacer,
EuiText,
EuiCodeBlock,
EuiTabbedContent,
EuiBetaBadge,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { CreateAgentInstructions } from './agent_instructions_mappings';
import React, { ComponentType } from 'react';
import styled from 'styled-components';
import {
AgentRuntimeAttachmentProps,
CreateAgentInstructions,
} from './agent_instructions_mappings';
import {
Markdown,
useKibana,
} from '../../../../../../../src/plugins/kibana_react/public';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import { AgentIcon } from '../../shared/agent_icon';
import { NewPackagePolicy } from '../apm_policy_form/typings';
import type {
NewPackagePolicy,
PackagePolicy,
PackagePolicyEditExtensionComponentProps,
} from '../apm_policy_form/typings';
import { getCommands } from '../../../tutorial/config_agent/commands/get_commands';
import { replaceTemplateStrings } from './replace_template_strings';
import { renderMustache } from './render_mustache';
function AccordionButtonContent({
agentName,
@ -97,96 +107,175 @@ function TutorialConfigAgent({
}
interface Props {
policy: PackagePolicy;
newPolicy: NewPackagePolicy;
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
agentName: AgentName;
title: string;
variantId: string;
createAgentInstructions: CreateAgentInstructions;
AgentRuntimeAttachment?: ComponentType<AgentRuntimeAttachmentProps>;
}
const StyledEuiAccordion = styled(EuiAccordion)`
// This is an alternative fix suggested by the EUI team to fix drag elements inside EuiAccordion
// This Issue tracks the fix on the Eui side https://github.com/elastic/eui/issues/3548#issuecomment-639041283
.euiAccordion__childWrapper {
transform: none;
}
`;
export function AgentInstructionsAccordion({
policy,
newPolicy,
onChange,
agentName,
title,
createAgentInstructions,
variantId,
AgentRuntimeAttachment,
}: Props) {
const docLinks = useKibana().services.docLinks;
const vars = newPolicy?.inputs?.[0]?.vars;
const apmServerUrl = vars?.url.value;
const secretToken = vars?.secret_token.value;
const steps = createAgentInstructions(apmServerUrl, secretToken);
const stepsElements = steps.map(
(
{ title: stepTitle, textPre, textPost, customComponentName, commands },
index
) => {
const commandBlock = commands
? renderMustache({
text: commands,
docLinks,
})
: '';
return (
<section key={index}>
<EuiText>
<h4>{stepTitle}</h4>
</EuiText>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{textPre && (
<InstructionsContent
markdown={renderMustache({ text: textPre, docLinks })}
/>
)}
{commandBlock && (
<>
<EuiSpacer size="s" />
<EuiCodeBlock isCopyable language="bash">
{commandBlock}
</EuiCodeBlock>
</>
)}
{customComponentName === 'TutorialConfigAgent' && (
<TutorialConfigAgent
variantId={variantId}
apmServerUrl={apmServerUrl}
secretToken={secretToken}
/>
)}
{customComponentName === 'TutorialConfigAgentRumScript' && (
<TutorialConfigAgent
variantId="js_script"
apmServerUrl={apmServerUrl}
secretToken={secretToken}
/>
)}
{textPost && (
<>
<EuiSpacer />
<InstructionsContent
markdown={renderMustache({ text: textPost, docLinks })}
/>
</>
)}
</EuiText>
<EuiSpacer />
</section>
);
}
);
const manualInstrumentationContent = (
<>
<EuiSpacer />
{stepsElements}
</>
);
return (
<EuiAccordion
<StyledEuiAccordion
id={agentName}
buttonContent={
<AccordionButtonContent agentName={agentName} title={title} />
}
>
<EuiSpacer />
{steps.map(
(
{
title: stepTitle,
textPre,
textPost,
customComponentName,
commands,
},
index
) => {
const commandBlock = replaceTemplateStrings(
Array.isArray(commands) ? commands.join('\n') : commands || '',
docLinks
);
return (
<section key={index}>
<EuiText>
<h4>{stepTitle}</h4>
</EuiText>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{textPre && (
<InstructionsContent
markdown={replaceTemplateStrings(textPre, docLinks)}
/>
)}
{commandBlock && (
<>
<EuiSpacer size="s" />
<EuiCodeBlock isCopyable language="bash">
{commandBlock}
</EuiCodeBlock>
</>
)}
{customComponentName === 'TutorialConfigAgent' && (
<TutorialConfigAgent
variantId={variantId}
apmServerUrl={apmServerUrl}
secretToken={secretToken}
/>
)}
{customComponentName === 'TutorialConfigAgentRumScript' && (
<TutorialConfigAgent
variantId="js_script"
apmServerUrl={apmServerUrl}
secretToken={secretToken}
/>
)}
{textPost && (
{AgentRuntimeAttachment ? (
<>
<EuiSpacer />
<EuiTabbedContent
tabs={[
{
id: 'manual-instrumentation',
name: i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.manualInstrumentation',
{ defaultMessage: 'Manual instrumentation' }
),
content: manualInstrumentationContent,
},
{
id: 'auto-attachment',
name: (
<EuiFlexGroup
justifyContent="flexStart"
alignItems="baseline"
gutterSize="s"
>
<EuiFlexItem grow={false}>
{i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.autoAttachment',
{ defaultMessage: 'Auto-Attachment' }
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.betaBadge.label',
{ defaultMessage: 'BETA' }
)}
tooltipContent={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.betaBadge.tooltipContent',
{
defaultMessage:
'Auto-attachment for Java is not GA. Please help us by reporting any bugs.',
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
),
content: (
<>
<EuiSpacer />
<InstructionsContent
markdown={replaceTemplateStrings(textPost, docLinks)}
<AgentRuntimeAttachment
policy={policy}
newPolicy={newPolicy}
onChange={onChange}
/>
</>
)}
</EuiText>
<EuiSpacer />
</section>
);
}
),
},
]}
/>
</>
) : (
manualInstrumentationContent
)}
</EuiAccordion>
</StyledEuiAccordion>
);
}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { ComponentType } from 'react';
import {
createDotNetAgentInstructions,
createDjangoAgentInstructions,
@ -18,6 +19,18 @@ import {
createRackAgentInstructions,
} from '../../../../common/tutorial/instructions/apm_agent_instructions';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import { JavaRuntimeAttachment } from './runtime_attachment/supported_agents/java_runtime_attachment';
import {
NewPackagePolicy,
PackagePolicy,
PackagePolicyEditExtensionComponentProps,
} from '../apm_policy_form/typings';
export interface AgentRuntimeAttachmentProps {
policy: PackagePolicy;
newPolicy: NewPackagePolicy;
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
}
export type CreateAgentInstructions = (
apmServerUrl?: string,
@ -35,12 +48,14 @@ export const ApmAgentInstructionsMappings: Array<{
title: string;
variantId: string;
createAgentInstructions: CreateAgentInstructions;
AgentRuntimeAttachment?: ComponentType<AgentRuntimeAttachmentProps>;
}> = [
{
agentName: 'java',
title: 'Java',
variantId: 'java',
createAgentInstructions: createJavaAgentInstructions,
AgentRuntimeAttachment: JavaRuntimeAttachment,
},
{
agentName: 'rum-js',

View file

@ -21,19 +21,28 @@ interface Props {
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
}
export function ApmAgents({ newPolicy }: Props) {
export function ApmAgents({ policy, newPolicy, onChange }: Props) {
return (
<div>
{ApmAgentInstructionsMappings.map(
({ agentName, title, createAgentInstructions, variantId }) => (
({
agentName,
title,
createAgentInstructions,
variantId,
AgentRuntimeAttachment,
}) => (
<Fragment key={agentName}>
<EuiPanel>
<AgentInstructionsAccordion
agentName={agentName}
title={title}
newPolicy={newPolicy}
createAgentInstructions={createAgentInstructions}
variantId={variantId}
AgentRuntimeAttachment={AgentRuntimeAttachment}
policy={policy}
newPolicy={newPolicy}
onChange={onChange}
/>
</EuiPanel>
<EuiSpacer />

View file

@ -10,12 +10,17 @@ import Mustache from 'mustache';
const TEMPLATE_TAGS = ['{', '}'];
export function replaceTemplateStrings(
text: string,
docLinks?: CoreStart['docLinks']
) {
Mustache.parse(text, TEMPLATE_TAGS);
return Mustache.render(text, {
export function renderMustache({
text,
docLinks,
}: {
text: string | string[];
docLinks?: CoreStart['docLinks'];
}) {
const template = Array.isArray(text) ? text.join('\n') : text;
Mustache.parse(template, TEMPLATE_TAGS);
return Mustache.render(template, {
config: {
docs: {
base_url: docLinks?.ELASTIC_WEBSITE_URL,

View file

@ -0,0 +1,30 @@
/*
* 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 {
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiBadge,
} from '@elastic/eui';
import React from 'react';
export function DefaultDiscoveryRule() {
return (
<EuiPanel paddingSize="m" style={{ margin: 4 }} hasBorder={true}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false} style={{ marginLeft: 100 }}>
<EuiBadge color="danger">Exclude</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s">Everything else</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -0,0 +1,125 @@
/*
* 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 {
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiBadge,
EuiPanel,
DraggableProvidedDragHandleProps,
EuiButtonIcon,
} from '@elastic/eui';
import React, { useMemo } from 'react';
import { Operation } from '.';
interface Props {
id: string;
order: number;
operation: string;
type: string;
probe: string;
providedDragHandleProps?: DraggableProvidedDragHandleProps;
onDelete: (discoveryItemId: string) => void;
onEdit: (discoveryItemId: string) => void;
operationTypes: Operation[];
}
export function DiscoveryRule({
id,
order,
operation,
type,
probe,
providedDragHandleProps,
onDelete,
onEdit,
operationTypes,
}: Props) {
const operationTypesLabels = useMemo(() => {
return operationTypes.reduce<{
[operationValue: string]: {
label: string;
types: { [typeValue: string]: string };
};
}>((acc, current) => {
return {
...acc,
[current.operation.value]: {
label: current.operation.label,
types: current.types.reduce((memo, { value, label }) => {
return { ...memo, [value]: label };
}, {}),
},
};
}, {});
}, [operationTypes]);
return (
<EuiPanel paddingSize="m" hasBorder={true}>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<div
{...providedDragHandleProps}
aria-label={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.discoveryRule.DragHandle',
{ defaultMessage: 'Drag Handle' }
)}
>
<EuiIcon type="grab" />
</div>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="baseline">
<EuiFlexItem grow={false}>
<EuiBadge>{order}</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge color={operation === 'exclude' ? 'danger' : 'success'}>
{operationTypesLabels[operation].label}
</EuiBadge>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText>
<h4>{operationTypesLabels[operation].types[type]}</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>{probe}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginLeft: 'auto' }}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="pencil"
color="primary"
onClick={() => {
onEdit(id);
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="trash"
color="danger"
onClick={() => {
onDelete(id);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -0,0 +1,181 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiButton,
EuiButtonEmpty,
EuiFormFieldset,
EuiSelect,
EuiFieldText,
EuiFormRow,
EuiSuperSelect,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
Operation,
DISCOVERY_RULE_TYPE_ALL,
STAGED_DISCOVERY_RULE_ID,
} from '.';
interface Props {
id: string;
onChangeOperation: (discoveryItemId: string) => void;
operation: string;
onChangeType: (discoveryItemId: string) => void;
type: string;
onChangeProbe: (discoveryItemId: string) => void;
probe: string;
onCancel: () => void;
onSubmit: () => void;
operationTypes: Operation[];
}
export function EditDiscoveryRule({
id,
onChangeOperation,
operation,
onChangeType,
type,
onChangeProbe,
probe,
onCancel,
onSubmit,
operationTypes,
}: Props) {
return (
<EuiPanel paddingSize="m" hasBorder={true}>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiFormFieldset
legend={{
children: i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.editDisacoveryRule.operation',
{ defaultMessage: 'Operation' }
),
}}
>
<EuiSelect
options={operationTypes.map((item) => ({
text: item.operation.label,
value: item.operation.value,
}))}
value={operation}
onChange={(e) => {
onChangeOperation(e.target.value);
}}
/>
</EuiFormFieldset>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormFieldset
legend={{
children: i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.editDisacoveryRule.type',
{ defaultMessage: 'Type' }
),
}}
>
<EuiFormRow
fullWidth
helpText={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.helpText',
{
defaultMessage: 'Choose from allowed parameters',
}
)}
>
<EuiSuperSelect
hasDividers
fullWidth
options={
operationTypes
.find(
({ operation: definedOperation }) =>
definedOperation.value === operation
)
?.types.map((item) => ({
inputDisplay: item.label,
value: item.value,
dropdownDisplay: (
<>
<strong>{item.label}</strong>
<EuiText size="s" color="subdued">
<p>{item.description}</p>
</EuiText>
</>
),
})) ?? []
}
valueOfSelected={type}
onChange={onChangeType}
/>
</EuiFormRow>
</EuiFormFieldset>
</EuiFlexItem>
</EuiFlexGroup>
{type !== DISCOVERY_RULE_TYPE_ALL && (
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormFieldset
legend={{
children: i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.editDisacoveryRule.probe',
{ defaultMessage: 'Probe' }
),
}}
>
<EuiFormRow
fullWidth
helpText={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.probeValue',
{
defaultMessage: 'Enter the probe value',
}
)}
>
<EuiFieldText
fullWidth
value={probe}
onChange={(e) => onChangeProbe(e.target.value)}
/>
</EuiFormRow>
</EuiFormFieldset>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onCancel}>Cancel</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSubmit}
fill
disabled={type === DISCOVERY_RULE_TYPE_ALL ? false : probe === ''}
>
{id === STAGED_DISCOVERY_RULE_ID
? i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.add',
{ defaultMessage: 'Add' }
)
: i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.editRule.save',
{ defaultMessage: 'Save' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -0,0 +1,327 @@
/*
* 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 {
htmlIdGenerator,
euiDragDropReorder,
DropResult,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import React, { useState, useCallback, ReactNode } from 'react';
import { RuntimeAttachment as RuntimeAttachmentStateless } from './runtime_attachment';
export const STAGED_DISCOVERY_RULE_ID = 'STAGED_DISCOVERY_RULE_ID';
export const DISCOVERY_RULE_TYPE_ALL = 'all';
export interface IDiscoveryRule {
operation: string;
type: string;
probe: string;
}
export type IDiscoveryRuleList = Array<{
id: string;
discoveryRule: IDiscoveryRule;
}>;
export interface RuntimeAttachmentSettings {
enabled: boolean;
discoveryRules: IDiscoveryRule[];
version: string | null;
}
interface Props {
onChange?: (runtimeAttachmentSettings: RuntimeAttachmentSettings) => void;
toggleDescription: ReactNode;
discoveryRulesDescription: ReactNode;
showUnsavedWarning?: boolean;
initialIsEnabled?: boolean;
initialDiscoveryRules?: IDiscoveryRule[];
operationTypes: Operation[];
selectedVersion: string;
versions: string[];
}
interface Option {
value: string;
label: string;
description?: string;
}
export interface Operation {
operation: Option;
types: Option[];
}
const versionRegex = new RegExp(/^\d+\.\d+\.\d+$/);
function validateVersion(version: string) {
return versionRegex.test(version);
}
export function RuntimeAttachment(props: Props) {
const { initialDiscoveryRules = [], onChange = () => {} } = props;
const [isEnabled, setIsEnabled] = useState(Boolean(props.initialIsEnabled));
const [discoveryRuleList, setDiscoveryRuleList] =
useState<IDiscoveryRuleList>(
initialDiscoveryRules.map((discoveryRule) => ({
id: generateId(),
discoveryRule,
}))
);
const [editDiscoveryRuleId, setEditDiscoveryRuleId] = useState<null | string>(
null
);
const [version, setVersion] = useState(props.selectedVersion);
const [versions, setVersions] = useState(props.versions);
const [isValidVersion, setIsValidVersion] = useState(
validateVersion(version)
);
const onToggleEnable = useCallback(() => {
const nextIsEnabled = !isEnabled;
setIsEnabled(nextIsEnabled);
onChange({
enabled: nextIsEnabled,
discoveryRules: nextIsEnabled
? discoveryRuleList.map(({ discoveryRule }) => discoveryRule)
: [],
version: nextIsEnabled ? version : null,
});
}, [isEnabled, onChange, discoveryRuleList, version]);
const onDelete = useCallback(
(discoveryRuleId: string) => {
const filteredDiscoveryRuleList = discoveryRuleList.filter(
({ id }) => id !== discoveryRuleId
);
setDiscoveryRuleList(filteredDiscoveryRuleList);
onChange({
enabled: isEnabled,
discoveryRules: filteredDiscoveryRuleList.map(
({ discoveryRule }) => discoveryRule
),
version,
});
},
[isEnabled, discoveryRuleList, onChange, version]
);
const onEdit = useCallback(
(discoveryRuleId: string) => {
const editingDiscoveryRule = discoveryRuleList.find(
({ id }) => id === discoveryRuleId
);
if (editingDiscoveryRule) {
const {
discoveryRule: { operation, type, probe },
} = editingDiscoveryRule;
setStagedOperationText(operation);
setStagedTypeText(type);
setStagedProbeText(probe);
setEditDiscoveryRuleId(discoveryRuleId);
}
},
[discoveryRuleList]
);
const [stagedOperationText, setStagedOperationText] = useState('');
const [stagedTypeText, setStagedTypeText] = useState('');
const [stagedProbeText, setStagedProbeText] = useState('');
const onChangeOperation = useCallback(
(operationText: string) => {
setStagedOperationText(operationText);
const selectedOperationTypes = props.operationTypes.find(
({ operation }) => operationText === operation.value
);
const selectedTypeAvailable = selectedOperationTypes?.types.some(
({ value }) => stagedTypeText === value
);
if (!selectedTypeAvailable) {
setStagedTypeText(selectedOperationTypes?.types[0].value ?? '');
}
},
[props.operationTypes, stagedTypeText]
);
const onChangeType = useCallback((operationText: string) => {
setStagedTypeText(operationText);
if (operationText === DISCOVERY_RULE_TYPE_ALL) {
setStagedProbeText('');
}
}, []);
const onChangeProbe = useCallback((operationText: string) => {
setStagedProbeText(operationText);
}, []);
const onCancel = useCallback(() => {
if (editDiscoveryRuleId === STAGED_DISCOVERY_RULE_ID) {
onDelete(STAGED_DISCOVERY_RULE_ID);
}
setEditDiscoveryRuleId(null);
}, [editDiscoveryRuleId, onDelete]);
const onSubmit = useCallback(() => {
const editDiscoveryRuleIndex = discoveryRuleList.findIndex(
({ id }) => id === editDiscoveryRuleId
);
const editDiscoveryRule = discoveryRuleList[editDiscoveryRuleIndex];
const nextDiscoveryRuleList = [
...discoveryRuleList.slice(0, editDiscoveryRuleIndex),
{
id:
editDiscoveryRule.id === STAGED_DISCOVERY_RULE_ID
? generateId()
: editDiscoveryRule.id,
discoveryRule: {
operation: stagedOperationText,
type: stagedTypeText,
probe: stagedProbeText,
},
},
...discoveryRuleList.slice(editDiscoveryRuleIndex + 1),
];
setDiscoveryRuleList(nextDiscoveryRuleList);
setEditDiscoveryRuleId(null);
onChange({
enabled: isEnabled,
discoveryRules: nextDiscoveryRuleList.map(
({ discoveryRule }) => discoveryRule
),
version,
});
}, [
isEnabled,
editDiscoveryRuleId,
stagedOperationText,
stagedTypeText,
stagedProbeText,
discoveryRuleList,
onChange,
version,
]);
const onAddRule = useCallback(() => {
const firstOperationType = props.operationTypes[0];
const operationText = firstOperationType.operation.value;
const typeText = firstOperationType.types[0].value;
const valueText = '';
setStagedOperationText(operationText);
setStagedTypeText(typeText);
setStagedProbeText(valueText);
const nextDiscoveryRuleList = [
{
id: STAGED_DISCOVERY_RULE_ID,
discoveryRule: {
operation: operationText,
type: typeText,
probe: valueText,
},
},
...discoveryRuleList,
];
setDiscoveryRuleList(nextDiscoveryRuleList);
setEditDiscoveryRuleId(STAGED_DISCOVERY_RULE_ID);
}, [discoveryRuleList, props.operationTypes]);
const onDragEnd = useCallback(
({ source, destination }: DropResult) => {
if (source && destination) {
const nextDiscoveryRuleList = euiDragDropReorder(
discoveryRuleList,
source.index,
destination.index
);
setDiscoveryRuleList(nextDiscoveryRuleList);
onChange({
enabled: isEnabled,
discoveryRules: nextDiscoveryRuleList.map(
({ discoveryRule }) => discoveryRule
),
version,
});
}
},
[isEnabled, discoveryRuleList, onChange, version]
);
function onChangeVersion(nextVersion?: string) {
if (!nextVersion) {
return;
}
setVersion(nextVersion);
onChange({
enabled: isEnabled,
discoveryRules: isEnabled
? discoveryRuleList.map(({ discoveryRule }) => discoveryRule)
: [],
version: nextVersion,
});
}
function onCreateNewVersion(
newVersion: string,
flattenedOptions: Array<EuiComboBoxOptionOption<string>>
) {
const normalizedNewVersion = newVersion.trim().toLowerCase();
const isNextVersionValid = validateVersion(normalizedNewVersion);
setIsValidVersion(isNextVersionValid);
if (!normalizedNewVersion || !isNextVersionValid) {
return;
}
// Create the option if it doesn't exist.
if (
flattenedOptions.findIndex(
(option) => option.label.trim().toLowerCase() === normalizedNewVersion
) === -1
) {
setVersions([...versions, newVersion]);
}
onChangeVersion(newVersion);
}
return (
<RuntimeAttachmentStateless
isEnabled={isEnabled}
onToggleEnable={onToggleEnable}
discoveryRuleList={discoveryRuleList}
setDiscoveryRuleList={setDiscoveryRuleList}
onDelete={onDelete}
editDiscoveryRuleId={editDiscoveryRuleId}
onEdit={onEdit}
onChangeOperation={onChangeOperation}
stagedOperationText={stagedOperationText}
onChangeType={onChangeType}
stagedTypeText={stagedTypeText}
onChangeProbe={onChangeProbe}
stagedProbeText={stagedProbeText}
onCancel={onCancel}
onSubmit={onSubmit}
onAddRule={onAddRule}
operationTypes={props.operationTypes}
toggleDescription={props.toggleDescription}
discoveryRulesDescription={props.discoveryRulesDescription}
showUnsavedWarning={props.showUnsavedWarning}
onDragEnd={onDragEnd}
selectedVersion={version}
versions={versions}
onChangeVersion={(selectedVersions) => {
const nextVersion: string | undefined = selectedVersions[0]?.label;
const isNextVersionValid = validateVersion(nextVersion);
setIsValidVersion(isNextVersionValid);
onChangeVersion(nextVersion);
}}
onCreateNewVersion={onCreateNewVersion}
isValidVersion={isValidVersion}
/>
);
}
const generateId = htmlIdGenerator();

View file

@ -0,0 +1,484 @@
/*
* 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 { Meta, Story } from '@storybook/react';
import React, { useState } from 'react';
import { RuntimeAttachment } from '.';
import { JavaRuntimeAttachment } from './supported_agents/java_runtime_attachment';
const stories: Meta<{}> = {
title: 'fleet/Runtime agent attachment',
component: RuntimeAttachment,
decorators: [
(StoryComponent) => {
return (
<div style={{ width: 700 }}>
<StoryComponent />
</div>
);
},
],
};
export default stories;
const excludeOptions = [
{ value: 'main', label: 'main class / jar name' },
{ value: 'vmarg', label: 'vmarg' },
{ value: 'user', label: 'user' },
];
const includeOptions = [{ value: 'all', label: 'All' }, ...excludeOptions];
const versions = ['1.27.1', '1.27.0', '1.26.0', '1.25.0'];
export const RuntimeAttachmentExample: Story = () => {
const [runtimeAttachmentSettings, setRuntimeAttachmentSettings] = useState(
{}
);
return (
<>
<RuntimeAttachment
operationTypes={[
{
operation: { value: 'include', label: 'Include' },
types: includeOptions,
},
{
operation: { value: 'exclude', label: 'Exclude' },
types: excludeOptions,
},
]}
onChange={(settings: any) => {
setRuntimeAttachmentSettings(settings);
}}
toggleDescription="Attach the Java agent to running and starting Java applications."
discoveryRulesDescription="For every running JVM, the discovery rules are evaluated in the order they are provided. The first matching rule determines the outcome. Learn more in the docs"
showUnsavedWarning={true}
initialIsEnabled={true}
initialDiscoveryRules={[
{
operation: 'include',
type: 'main',
probe: 'java-opbeans-10010',
},
{
operation: 'exclude',
type: 'vmarg',
probe: '10948653898867',
},
]}
versions={versions}
selectedVersion={versions[0]}
/>
<hr />
<pre>{JSON.stringify(runtimeAttachmentSettings, null, 4)}</pre>
</>
);
};
export const JavaRuntimeAttachmentExample: Story = () => {
return (
<JavaRuntimeAttachment
policy={policy}
newPolicy={newPolicy}
onChange={() => {}}
/>
);
};
const policy = {
id: 'cc380ec5-d84e-40e1-885a-d706edbdc968',
version: 'WzM0MzA2LDJd',
name: 'apm-1',
description: '',
namespace: 'default',
policy_id: 'policy-elastic-agent-on-cloud',
enabled: true,
output_id: '',
inputs: [
{
type: 'apm',
policy_template: 'apmserver',
enabled: true,
streams: [],
vars: {
host: {
value: 'localhost:8200',
type: 'text',
},
url: {
value: 'http://localhost:8200',
type: 'text',
},
secret_token: {
type: 'text',
},
api_key_enabled: {
value: false,
type: 'bool',
},
enable_rum: {
value: true,
type: 'bool',
},
anonymous_enabled: {
value: true,
type: 'bool',
},
anonymous_allow_agent: {
value: ['rum-js', 'js-base', 'iOS/swift'],
type: 'text',
},
anonymous_allow_service: {
value: [],
type: 'text',
},
anonymous_rate_limit_event_limit: {
value: 10,
type: 'integer',
},
anonymous_rate_limit_ip_limit: {
value: 10000,
type: 'integer',
},
default_service_environment: {
type: 'text',
},
rum_allow_origins: {
value: ['"*"'],
type: 'text',
},
rum_allow_headers: {
value: [],
type: 'text',
},
rum_response_headers: {
type: 'yaml',
},
rum_library_pattern: {
value: '"node_modules|bower_components|~"',
type: 'text',
},
rum_exclude_from_grouping: {
value: '"^/webpack"',
type: 'text',
},
api_key_limit: {
value: 100,
type: 'integer',
},
max_event_bytes: {
value: 307200,
type: 'integer',
},
capture_personal_data: {
value: true,
type: 'bool',
},
max_header_bytes: {
value: 1048576,
type: 'integer',
},
idle_timeout: {
value: '45s',
type: 'text',
},
read_timeout: {
value: '3600s',
type: 'text',
},
shutdown_timeout: {
value: '30s',
type: 'text',
},
write_timeout: {
value: '30s',
type: 'text',
},
max_connections: {
value: 0,
type: 'integer',
},
response_headers: {
type: 'yaml',
},
expvar_enabled: {
value: false,
type: 'bool',
},
tls_enabled: {
value: false,
type: 'bool',
},
tls_certificate: {
type: 'text',
},
tls_key: {
type: 'text',
},
tls_supported_protocols: {
value: ['TLSv1.0', 'TLSv1.1', 'TLSv1.2'],
type: 'text',
},
tls_cipher_suites: {
value: [],
type: 'text',
},
tls_curve_types: {
value: [],
type: 'text',
},
tail_sampling_policies: {
type: 'yaml',
},
tail_sampling_interval: {
type: 'text',
},
},
config: {
'apm-server': {
value: {
rum: {
source_mapping: {
metadata: [],
},
},
agent_config: [],
},
},
},
compiled_input: {
'apm-server': {
auth: {
anonymous: {
allow_agent: ['rum-js', 'js-base', 'iOS/swift'],
allow_service: null,
enabled: true,
rate_limit: {
event_limit: 10,
ip_limit: 10000,
},
},
api_key: {
enabled: false,
limit: 100,
},
secret_token: null,
},
capture_personal_data: true,
idle_timeout: '45s',
default_service_environment: null,
'expvar.enabled': false,
host: 'localhost:8200',
max_connections: 0,
max_event_size: 307200,
max_header_size: 1048576,
read_timeout: '3600s',
response_headers: null,
rum: {
allow_headers: null,
allow_origins: ['*'],
enabled: true,
exclude_from_grouping: '^/webpack',
library_pattern: 'node_modules|bower_components|~',
response_headers: null,
},
shutdown_timeout: '30s',
write_timeout: '30s',
},
},
},
],
package: {
name: 'apm',
title: 'Elastic APM',
version: '7.16.0',
},
elasticsearch: {
privileges: {
cluster: ['cluster:monitor/main'],
},
},
revision: 1,
created_at: '2021-11-18T02:14:55.758Z',
created_by: 'admin',
updated_at: '2021-11-18T02:14:55.758Z',
updated_by: 'admin',
};
const newPolicy = {
version: 'WzM0MzA2LDJd',
name: 'apm-1',
description: '',
namespace: 'default',
policy_id: 'policy-elastic-agent-on-cloud',
enabled: true,
output_id: '',
package: {
name: 'apm',
title: 'Elastic APM',
version: '8.0.0-dev2',
},
elasticsearch: {
privileges: {
cluster: ['cluster:monitor/main'],
},
},
inputs: [
{
type: 'apm',
policy_template: 'apmserver',
enabled: true,
vars: {
host: {
value: 'localhost:8200',
type: 'text',
},
url: {
value: 'http://localhost:8200',
type: 'text',
},
secret_token: {
type: 'text',
},
api_key_enabled: {
value: false,
type: 'bool',
},
enable_rum: {
value: true,
type: 'bool',
},
anonymous_enabled: {
value: true,
type: 'bool',
},
anonymous_allow_agent: {
value: ['rum-js', 'js-base', 'iOS/swift'],
type: 'text',
},
anonymous_allow_service: {
value: [],
type: 'text',
},
anonymous_rate_limit_event_limit: {
value: 10,
type: 'integer',
},
anonymous_rate_limit_ip_limit: {
value: 10000,
type: 'integer',
},
default_service_environment: {
type: 'text',
},
rum_allow_origins: {
value: ['"*"'],
type: 'text',
},
rum_allow_headers: {
value: [],
type: 'text',
},
rum_response_headers: {
type: 'yaml',
},
rum_library_pattern: {
value: '"node_modules|bower_components|~"',
type: 'text',
},
rum_exclude_from_grouping: {
value: '"^/webpack"',
type: 'text',
},
api_key_limit: {
value: 100,
type: 'integer',
},
max_event_bytes: {
value: 307200,
type: 'integer',
},
capture_personal_data: {
value: true,
type: 'bool',
},
max_header_bytes: {
value: 1048576,
type: 'integer',
},
idle_timeout: {
value: '45s',
type: 'text',
},
read_timeout: {
value: '3600s',
type: 'text',
},
shutdown_timeout: {
value: '30s',
type: 'text',
},
write_timeout: {
value: '30s',
type: 'text',
},
max_connections: {
value: 0,
type: 'integer',
},
response_headers: {
type: 'yaml',
},
expvar_enabled: {
value: false,
type: 'bool',
},
tls_enabled: {
value: false,
type: 'bool',
},
tls_certificate: {
type: 'text',
},
tls_key: {
type: 'text',
},
tls_supported_protocols: {
value: ['TLSv1.0', 'TLSv1.1', 'TLSv1.2'],
type: 'text',
},
tls_cipher_suites: {
value: [],
type: 'text',
},
tls_curve_types: {
value: [],
type: 'text',
},
tail_sampling_policies: {
type: 'yaml',
},
tail_sampling_interval: {
type: 'text',
},
},
config: {
'apm-server': {
value: {
rum: {
source_mapping: {
metadata: [],
},
},
agent_config: [],
},
},
},
streams: [],
},
],
};

View file

@ -0,0 +1,235 @@
/*
* 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,
EuiSpacer,
EuiSwitch,
EuiText,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiDragDropContext,
EuiDroppable,
EuiDraggable,
EuiIcon,
DropResult,
EuiComboBox,
EuiComboBoxProps,
EuiFormRow,
} from '@elastic/eui';
import React, { ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { DiscoveryRule } from './discovery_rule';
import { DefaultDiscoveryRule } from './default_discovery_rule';
import { EditDiscoveryRule } from './edit_discovery_rule';
import { IDiscoveryRuleList, Operation } from '.';
interface Props {
isEnabled: boolean;
onToggleEnable: () => void;
discoveryRuleList: IDiscoveryRuleList;
setDiscoveryRuleList: (discoveryRuleItems: IDiscoveryRuleList) => void;
onDelete: (discoveryItemId: string) => void;
editDiscoveryRuleId: null | string;
onEdit: (discoveryItemId: string) => void;
onChangeOperation: (operationText: string) => void;
stagedOperationText: string;
onChangeType: (typeText: string) => void;
stagedTypeText: string;
onChangeProbe: (probeText: string) => void;
stagedProbeText: string;
onCancel: () => void;
onSubmit: () => void;
onAddRule: () => void;
operationTypes: Operation[];
toggleDescription: ReactNode;
discoveryRulesDescription: ReactNode;
showUnsavedWarning?: boolean;
onDragEnd: (dropResult: DropResult) => void;
selectedVersion: string;
versions: string[];
onChangeVersion: EuiComboBoxProps<string>['onChange'];
onCreateNewVersion: EuiComboBoxProps<string>['onCreateOption'];
isValidVersion: boolean;
}
export function RuntimeAttachment({
isEnabled,
onToggleEnable,
discoveryRuleList,
setDiscoveryRuleList,
onDelete,
editDiscoveryRuleId,
onEdit,
onChangeOperation,
stagedOperationText,
onChangeType,
stagedTypeText,
onChangeProbe,
stagedProbeText,
onCancel,
onSubmit,
onAddRule,
operationTypes,
toggleDescription,
discoveryRulesDescription,
showUnsavedWarning,
onDragEnd,
selectedVersion,
versions,
onChangeVersion,
onCreateNewVersion,
isValidVersion,
}: Props) {
return (
<div>
{showUnsavedWarning && (
<>
<EuiCallOut
title={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.unsavedRules',
{
defaultMessage:
'You have unsaved changes. Click "Save integration" to sync changes to the integration.',
}
)}
color="warning"
iconType="iInCircle"
size="s"
/>
<EuiSpacer />
</>
)}
<EuiFlexGroup>
<EuiFlexItem>
<EuiSwitch
label={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.enableRuntimeAttachement',
{ defaultMessage: 'Enable runtime attachment' }
)}
checked={isEnabled}
onChange={onToggleEnable}
/>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
<p>{toggleDescription}</p>
</EuiText>
</EuiFlexItem>
{isEnabled && versions && (
<EuiFlexItem>
<EuiFormRow
label={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.version',
{ defaultMessage: 'Version' }
)}
isInvalid={!isValidVersion}
error={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.version.invalid',
{ defaultMessage: 'Invalid version' }
)}
>
<EuiComboBox
selectedOptions={[{ label: selectedVersion }]}
placeholder={i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.version.placeHolder',
{ defaultMessage: 'Select or add a version' }
)}
options={versions.map((_version) => ({ label: _version }))}
onChange={onChangeVersion}
onCreateOption={onCreateNewVersion}
singleSelection
isClearable={false}
/>
</EuiFormRow>
</EuiFlexItem>
)}
</EuiFlexGroup>
{isEnabled && (
<>
<EuiSpacer size="l" />
<EuiText size="s">
<h3>
{i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.discoveryRules',
{ defaultMessage: 'Discovery rules' }
)}
</h3>
</EuiText>
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiIcon type="iInCircle" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="xs">
<p>{discoveryRulesDescription}</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconType="plusInCircle"
disabled={editDiscoveryRuleId !== null}
onClick={onAddRule}
>
{i18n.translate(
'xpack.apm.fleetIntegration.apmAgent.runtimeAttachment.addRule',
{ defaultMessage: 'Add rule' }
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiDroppable droppableId="RUNTIME_ATTACHMENT_DROPPABLE">
{discoveryRuleList.map(({ discoveryRule, id }, idx) => (
<EuiDraggable
spacing="m"
key={id}
index={idx}
draggableId={id}
customDragHandle={true}
>
{(provided) =>
id === editDiscoveryRuleId ? (
<EditDiscoveryRule
id={editDiscoveryRuleId}
onChangeOperation={onChangeOperation}
operation={stagedOperationText}
onChangeType={onChangeType}
type={stagedTypeText}
onChangeProbe={onChangeProbe}
probe={stagedProbeText}
onCancel={onCancel}
onSubmit={onSubmit}
operationTypes={operationTypes}
/>
) : (
<DiscoveryRule
id={id}
order={idx + 1}
operation={discoveryRule.operation}
type={discoveryRule.type}
probe={discoveryRule.probe}
providedDragHandleProps={provided.dragHandleProps}
onDelete={onDelete}
onEdit={onEdit}
operationTypes={operationTypes}
/>
)
}
</EuiDraggable>
))}
</EuiDroppable>
</EuiDragDropContext>
<DefaultDiscoveryRule />
</>
)}
<EuiSpacer size="s" />
</div>
);
}

View file

@ -0,0 +1,276 @@
/*
* 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 yaml from 'js-yaml';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useState, useMemo } from 'react';
import {
RuntimeAttachment,
RuntimeAttachmentSettings,
IDiscoveryRule,
} from '..';
import type {
NewPackagePolicy,
PackagePolicy,
PackagePolicyEditExtensionComponentProps,
} from '../../../apm_policy_form/typings';
interface Props {
policy: PackagePolicy;
newPolicy: NewPackagePolicy;
onChange: PackagePolicyEditExtensionComponentProps['onChange'];
}
const excludeOptions = [
{
value: 'main',
label: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.main',
{ defaultMessage: 'main' }
),
description: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.mainDescription',
{
defaultMessage:
'A regular expression of fully qualified main class names or paths to JARs of applications the java agent should be attached to. Performs a partial match so that foo matches /bin/foo.jar.',
}
),
},
{
value: 'vmarg',
label: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.vmarg',
{ defaultMessage: 'vmarg' }
),
description: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.vmargDescription',
{
defaultMessage:
'A regular expression matched against the arguments passed to the JVM, such as system properties. Performs a partial match so that attach=true matches the system property -Dattach=true.',
}
),
},
{
value: 'user',
label: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.user',
{ defaultMessage: 'user' }
),
description: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude.options.userDescription',
{
defaultMessage:
'A username that is matched against the operating system user that runs the JVM.',
}
),
},
];
const includeOptions = [
{
value: 'all',
label: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.include.options.all',
{ defaultMessage: 'All' }
),
description: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.include.options.allDescription',
{ defaultMessage: 'Includes all JVMs for attachment.' }
),
},
...excludeOptions,
];
const versions = [
'1.27.1',
'1.27.0',
'1.26.0',
'1.25.0',
'1.24.0',
'1.23.0',
'1.22.0',
'1.21.0',
'1.20.0',
'1.19.0',
'1.18.1',
'1.18.0',
'1.18.0.RC1',
'1.17.0',
'1.16.0',
'1.15.0',
'1.14.0',
'1.13.0',
'1.12.0',
'1.11.0',
'1.10.0',
'1.9.0',
'1.8.0',
'1.7.0',
'1.6.1',
'1.6.0',
'1.5.0',
'1.4.0',
'1.3.0',
'1.2.0',
];
function getApmVars(newPolicy: NewPackagePolicy) {
return newPolicy.inputs.find(({ type }) => type === 'apm')?.vars;
}
export function JavaRuntimeAttachment({ newPolicy, onChange }: Props) {
const [isDirty, setIsDirty] = useState(false);
const onChangePolicy = useCallback(
(runtimeAttachmentSettings: RuntimeAttachmentSettings) => {
const apmInputIdx = newPolicy.inputs.findIndex(
({ type }) => type === 'apm'
);
onChange({
isValid: true,
updatedPolicy: {
...newPolicy,
inputs: [
...newPolicy.inputs.slice(0, apmInputIdx),
{
...newPolicy.inputs[apmInputIdx],
vars: {
...newPolicy.inputs[apmInputIdx].vars,
java_attacher_enabled: {
value: runtimeAttachmentSettings.enabled,
type: 'bool',
},
java_attacher_discovery_rules: {
type: 'yaml',
value: encodeDiscoveryRulesYaml(
runtimeAttachmentSettings.discoveryRules
),
},
java_attacher_agent_version: {
type: 'text',
value: runtimeAttachmentSettings.version,
},
},
},
...newPolicy.inputs.slice(apmInputIdx + 1),
],
},
});
setIsDirty(true);
},
[newPolicy, onChange]
);
const apmVars = useMemo(() => getApmVars(newPolicy), [newPolicy]);
return (
<RuntimeAttachment
operationTypes={[
{
operation: {
value: 'include',
label: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.include',
{ defaultMessage: 'Include' }
),
},
types: includeOptions,
},
{
operation: {
value: 'exclude',
label: i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.operationType.exclude',
{ defaultMessage: 'Exclude' }
),
},
types: excludeOptions,
},
]}
onChange={onChangePolicy}
toggleDescription={i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.toggleDescription',
{
defaultMessage:
'Attach the Java agent to running and starting Java applications.',
}
)}
discoveryRulesDescription={
<FormattedMessage
id="xpack.apm.fleetIntegration.javaRuntime.discoveryRulesDescription"
defaultMessage="For every running JVM, the discovery rules are evaluated in the order they are provided. The first matching rule determines the outcome. Learn more in the {docLink}."
values={{
docLink: (
<a
href="https://www.elastic.co/guide/en/apm/agent/java/current/setup-attach-cli.html"
target="_blank"
>
{i18n.translate(
'xpack.apm.fleetIntegration.javaRuntime.discoveryRulesDescription.docLink',
{ defaultMessage: 'docs' }
)}
</a>
),
}}
/>
}
showUnsavedWarning={isDirty}
initialIsEnabled={apmVars?.java_attacher_enabled?.value}
initialDiscoveryRules={decodeDiscoveryRulesYaml(
apmVars?.java_attacher_discovery_rules?.value ?? '[]\n',
[initialDiscoveryRule]
)}
selectedVersion={
apmVars?.java_attacher_agent_version?.value || versions[0]
}
versions={versions}
/>
);
}
const initialDiscoveryRule = {
operation: 'include',
type: 'vmarg',
probe: 'elastic.apm.attach=true',
};
type DiscoveryRulesParsedYaml = Array<{ [operationType: string]: string }>;
function decodeDiscoveryRulesYaml(
discoveryRulesYaml: string,
defaultDiscoveryRules: IDiscoveryRule[] = []
): IDiscoveryRule[] {
try {
const parsedYaml: DiscoveryRulesParsedYaml =
yaml.load(discoveryRulesYaml) ?? [];
if (parsedYaml.length === 0) {
return defaultDiscoveryRules;
}
// transform into array of discovery rules
return parsedYaml.map((discoveryRuleMap) => {
const [operationType, probe] = Object.entries(discoveryRuleMap)[0];
return {
operation: operationType.split('-')[0],
type: operationType.split('-')[1],
probe,
};
});
} catch (error) {
return defaultDiscoveryRules;
}
}
function encodeDiscoveryRulesYaml(discoveryRules: IDiscoveryRule[]): string {
// transform into list of key,value objects for expected yaml result
const mappedDiscoveryRules: DiscoveryRulesParsedYaml = discoveryRules.map(
({ operation, type, probe }) => ({
[`${operation}-${type}`]: probe,
})
);
return yaml.dump(mappedDiscoveryRules);
}

View file

@ -348,7 +348,8 @@ export const EditPackagePolicyForm = memo<{
const [formState, setFormState] = useState<PackagePolicyFormState>('INVALID');
const savePackagePolicy = async () => {
setFormState('LOADING');
const result = await sendUpdatePackagePolicy(packagePolicyId, packagePolicy);
const { elasticsearch, ...restPackagePolicy } = packagePolicy; // ignore 'elasticsearch' property since it fails route validation
const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy);
setFormState('SUBMITTED');
return result;
};