mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Trace explorer (#131897)
* [APM] Trace explorer * Make sure tabs work on trace explorer * Add links to trace explorer from error detail view & service map * Add API tests * Fix failing E2E test * don't select edges when explorer is disabled * Fix lint error * Update x-pack/plugins/observability/server/ui_settings.ts Co-authored-by: Søren Louv-Jansen <sorenlouv@gmail.com> * Review feedback * Rename const in API tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Søren Louv-Jansen <sorenlouv@gmail.com>
This commit is contained in:
parent
3e8e89069f
commit
c16bcdc15d
67 changed files with 2478 additions and 406 deletions
|
@ -58,6 +58,8 @@ export interface EuiCodeEditorProps extends SupportedAriaAttributes, Omit<IAceEd
|
|||
*/
|
||||
mode?: IAceEditorProps['mode'] | object;
|
||||
id?: string;
|
||||
|
||||
onAceEditorRef?: (editor: AceEditor | null) => void;
|
||||
}
|
||||
|
||||
export interface EuiCodeEditorState {
|
||||
|
@ -98,6 +100,7 @@ class EuiCodeEditor extends Component<EuiCodeEditorProps, EuiCodeEditorState> {
|
|||
setOrRemoveAttribute(textbox, 'aria-labelledby', this.props['aria-labelledby']);
|
||||
setOrRemoveAttribute(textbox, 'aria-describedby', this.props['aria-describedby']);
|
||||
}
|
||||
this.props.onAceEditorRef?.(aceEditor);
|
||||
};
|
||||
|
||||
onEscToExit = () => {
|
||||
|
|
|
@ -5,4 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const APM_STATIC_INDEX_PATTERN_ID = 'apm_static_index_pattern_id';
|
||||
// value of const needs to be backwards compatible
|
||||
export const APM_STATIC_DATA_VIEW_ID = 'apm_static_index_pattern_id';
|
16
x-pack/plugins/apm/common/trace_explorer.ts
Normal file
16
x-pack/plugins/apm/common/trace_explorer.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 interface TraceSearchQuery {
|
||||
query: string;
|
||||
type: TraceSearchType;
|
||||
}
|
||||
|
||||
export enum TraceSearchType {
|
||||
kql = 'kql',
|
||||
eql = 'eql',
|
||||
}
|
|
@ -89,7 +89,7 @@ describe('Error details', () => {
|
|||
describe('when clicking on View x occurences in discover', () => {
|
||||
it('should redirects the user to discover', () => {
|
||||
cy.visit(errorDetailsPageHref);
|
||||
cy.contains('View 1 occurrence in Discover.').click();
|
||||
cy.contains('View 1 occurrence in Discover').click();
|
||||
cy.url().should('include', 'app/discover');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"observability",
|
||||
"ruleRegistry",
|
||||
"triggersActionsUi",
|
||||
"unifiedSearch"
|
||||
"unifiedSearch",
|
||||
"dataViews"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"actions",
|
||||
|
@ -33,12 +34,16 @@
|
|||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"configPath": ["xpack", "apm"],
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"apm"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"fleet",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"ml",
|
||||
"observability"
|
||||
"observability",
|
||||
"esUiShared"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -52,6 +52,8 @@ export const renderApp = ({
|
|||
inspector: pluginsStart.inspector,
|
||||
observability: pluginsStart.observability,
|
||||
observabilityRuleTypeRegistry,
|
||||
dataViews: pluginsStart.dataViews,
|
||||
unifiedSearch: pluginsStart.unifiedSearch,
|
||||
};
|
||||
|
||||
// render APM feedback link in global help menu
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DetailView should render Discover button 1`] = `
|
||||
<DiscoverErrorLink
|
||||
error={
|
||||
Object {
|
||||
"error": Object {
|
||||
"exception": Object {
|
||||
"handled": true,
|
||||
},
|
||||
},
|
||||
"http": Object {
|
||||
"request": Object {
|
||||
"method": "GET",
|
||||
},
|
||||
},
|
||||
"service": Object {
|
||||
"name": "myService",
|
||||
},
|
||||
"timestamp": Object {
|
||||
"us": 0,
|
||||
},
|
||||
"transaction": Object {
|
||||
"id": "myTransactionId",
|
||||
"sampled": true,
|
||||
},
|
||||
"url": Object {
|
||||
"full": "myUrl",
|
||||
},
|
||||
"user": Object {
|
||||
"id": "myUserId",
|
||||
},
|
||||
}
|
||||
}
|
||||
kuery=""
|
||||
>
|
||||
View 10 occurrences in Discover.
|
||||
</DiscoverErrorLink>
|
||||
`;
|
||||
|
||||
exports[`DetailView should render TabContent 1`] = `
|
||||
<TabContent
|
||||
currentTab={
|
||||
Object {
|
||||
"key": "exception_stacktrace",
|
||||
"label": "Exception stack trace",
|
||||
}
|
||||
}
|
||||
error={
|
||||
Object {
|
||||
"context": Object {},
|
||||
"error": Object {},
|
||||
"timestamp": Object {
|
||||
"us": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`DetailView should render tabs 1`] = `
|
||||
<EuiTabs>
|
||||
<EuiTab
|
||||
isSelected={true}
|
||||
key="exception_stacktrace"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Exception stack trace
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
isSelected={false}
|
||||
key="metadata"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Metadata
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
`;
|
|
@ -5,10 +5,33 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { mockMoment } from '../../../../utils/test_helpers';
|
||||
import { DetailView } from '.';
|
||||
import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: [
|
||||
'/services/opbeans-java/errors/0000?rangeFrom=now-15m&rangeTo=now',
|
||||
],
|
||||
});
|
||||
|
||||
function MockContext({ children }: { children: React.ReactElement }) {
|
||||
return (
|
||||
<EuiThemeProvider>
|
||||
<MockApmPluginContextWrapper history={history}>
|
||||
{children}
|
||||
</MockApmPluginContextWrapper>
|
||||
</EuiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function renderWithMockContext(element: React.ReactElement) {
|
||||
return render(element, { wrapper: MockContext });
|
||||
}
|
||||
|
||||
describe('DetailView', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -17,10 +40,10 @@ describe('DetailView', () => {
|
|||
});
|
||||
|
||||
it('should render empty state', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = renderWithMockContext(
|
||||
<DetailView errorGroup={{} as any} urlParams={{}} kuery="" />
|
||||
);
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
expect(wrapper.baseElement.innerHTML).toBe('<div></div>');
|
||||
});
|
||||
|
||||
it('should render Discover button', () => {
|
||||
|
@ -35,23 +58,25 @@ describe('DetailView', () => {
|
|||
url: { full: 'myUrl' },
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' },
|
||||
error: { exception: { handled: true } },
|
||||
error: { exception: [{ handled: true }] },
|
||||
transaction: { id: 'myTransactionId', sampled: true },
|
||||
} as any,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
const discoverLink = renderWithMockContext(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
).find('DiscoverErrorLink');
|
||||
).getByText(`View 10 occurrences in Discover`);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(discoverLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a Summary', () => {
|
||||
const errorGroup = {
|
||||
occurrencesCount: 10,
|
||||
error: {
|
||||
service: {
|
||||
name: 'opbeans-python',
|
||||
},
|
||||
error: {},
|
||||
timestamp: {
|
||||
us: 0,
|
||||
|
@ -59,11 +84,14 @@ describe('DetailView', () => {
|
|||
} as any,
|
||||
transaction: undefined,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
).find('Summary');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
const rendered = renderWithMockContext(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
);
|
||||
|
||||
expect(
|
||||
rendered.getByText('1337 minutes ago (mocking 0)')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render tabs', () => {
|
||||
|
@ -79,12 +107,14 @@ describe('DetailView', () => {
|
|||
user: {},
|
||||
} as any,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
).find('EuiTabs');
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
const rendered = renderWithMockContext(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
);
|
||||
|
||||
expect(rendered.getByText('Exception stack trace')).toBeInTheDocument();
|
||||
|
||||
expect(rendered.getByText('Metadata')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render TabContent', () => {
|
||||
|
@ -92,19 +122,23 @@ describe('DetailView', () => {
|
|||
occurrencesCount: 10,
|
||||
transaction: undefined,
|
||||
error: {
|
||||
service: {
|
||||
name: 'opbeans-python',
|
||||
},
|
||||
timestamp: {
|
||||
us: 0,
|
||||
},
|
||||
error: {},
|
||||
error: {
|
||||
exception: [{ handled: true }],
|
||||
},
|
||||
context: {},
|
||||
} as any,
|
||||
};
|
||||
const wrapper = shallow(
|
||||
const rendered = renderWithMockContext(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
).find('TabContent');
|
||||
);
|
||||
|
||||
expect(wrapper.exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(rendered.getByText('No stack trace available.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render without http request info', () => {
|
||||
|
@ -115,16 +149,20 @@ describe('DetailView', () => {
|
|||
timestamp: {
|
||||
us: 0,
|
||||
},
|
||||
error: {
|
||||
exception: [{ handled: true }],
|
||||
},
|
||||
http: { response: { status_code: 404 } },
|
||||
url: { full: 'myUrl' },
|
||||
service: { name: 'myService' },
|
||||
user: { id: 'myUserId' },
|
||||
error: { exception: { handled: true } },
|
||||
transaction: { id: 'myTransactionId', sampled: true },
|
||||
} as any,
|
||||
};
|
||||
expect(() =>
|
||||
shallow(<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />)
|
||||
renderWithMockContext(
|
||||
<DetailView errorGroup={errorGroup} urlParams={{}} kuery="" />
|
||||
)
|
||||
).not.toThrowError();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTab,
|
||||
|
@ -38,13 +41,12 @@ import {
|
|||
logStacktraceTab,
|
||||
} from './error_tabs';
|
||||
import { ExceptionStacktrace } from './exception_stacktrace';
|
||||
|
||||
const HeaderContainer = euiStyled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: ${({ theme }) => theme.eui.euiSize};
|
||||
`;
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { ERROR_GROUP_ID } from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { TraceSearchType } from '../../../../../common/trace_explorer';
|
||||
import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import { useTraceExplorerEnabledSetting } from '../../../../hooks/use_trace_explorer_enabled_setting';
|
||||
|
||||
const TransactionLinkName = euiStyled.div`
|
||||
margin-left: ${({ theme }) => theme.eui.euiSizeS};
|
||||
|
@ -73,6 +75,15 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) {
|
|||
|
||||
const { detailTab, offset, comparisonEnabled } = urlParams;
|
||||
|
||||
const router = useApmRouter();
|
||||
|
||||
const isTraceExplorerEnabled = useTraceExplorerEnabledSetting();
|
||||
|
||||
const {
|
||||
path: { groupId },
|
||||
query,
|
||||
} = useApmParams('/services/{serviceName}/errors/{groupId}');
|
||||
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
@ -85,30 +96,72 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) {
|
|||
const method = error.http?.request?.method;
|
||||
const status = error.http?.response?.status_code;
|
||||
|
||||
const traceExplorerLink = router.link('/traces/explorer', {
|
||||
query: {
|
||||
...query,
|
||||
query: `${ERROR_GROUP_ID}:${groupId}`,
|
||||
type: TraceSearchType.kql,
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
waterfallItemId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true}>
|
||||
<HeaderContainer>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.errorOccurrenceTitle',
|
||||
{
|
||||
defaultMessage: 'Error occurrence',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<DiscoverErrorLink error={error} kuery={kuery}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'View {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} in Discover.',
|
||||
values: { occurrencesCount },
|
||||
}
|
||||
)}
|
||||
</DiscoverErrorLink>
|
||||
</HeaderContainer>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.errorOccurrenceTitle',
|
||||
{
|
||||
defaultMessage: 'Error occurrence',
|
||||
}
|
||||
)}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{isTraceExplorerEnabled && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink href={traceExplorerLink}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiIcon type="apmTrace" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ whiteSpace: 'nowrap' }}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.viewOccurrencesInTraceExplorer',
|
||||
{
|
||||
defaultMessage: 'Explore traces with this error',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DiscoverErrorLink error={error} kuery={kuery}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiIcon type="discoverApp" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem style={{ whiteSpace: 'nowrap' }}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.errorGroupDetails.viewOccurrencesInDiscoverButtonLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'View {occurrencesCount} {occurrencesCount, plural, one {occurrence} other {occurrences}} in Discover',
|
||||
values: { occurrencesCount },
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</DiscoverErrorLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<Summary
|
||||
items={[
|
||||
|
|
|
@ -18,6 +18,7 @@ import React, {
|
|||
useState,
|
||||
} from 'react';
|
||||
import { useTheme } from '../../../hooks/use_theme';
|
||||
import { useTraceExplorerEnabledSetting } from '../../../hooks/use_trace_explorer_enabled_setting';
|
||||
import { getCytoscapeOptions } from './cytoscape_options';
|
||||
import { useCytoscapeEventHandlers } from './use_cytoscape_event_handlers';
|
||||
|
||||
|
@ -65,8 +66,9 @@ function CytoscapeComponent({
|
|||
style,
|
||||
}: CytoscapeProps) {
|
||||
const theme = useTheme();
|
||||
const isTraceExplorerEnabled = useTraceExplorerEnabledSetting();
|
||||
const [ref, cy] = useCytoscape({
|
||||
...getCytoscapeOptions(theme),
|
||||
...getCytoscapeOptions(theme, isTraceExplorerEnabled),
|
||||
elements,
|
||||
});
|
||||
useCytoscapeEventHandlers({ cy, serviceName, theme });
|
||||
|
|
|
@ -104,7 +104,10 @@ function isService(el: cytoscape.NodeSingular) {
|
|||
return el.data(SERVICE_NAME) !== undefined;
|
||||
}
|
||||
|
||||
const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => {
|
||||
const getStyle = (
|
||||
theme: EuiTheme,
|
||||
isTraceExplorerEnabled: boolean
|
||||
): cytoscape.Stylesheet[] => {
|
||||
const lineColor = theme.eui.euiColorMediumShade;
|
||||
return [
|
||||
{
|
||||
|
@ -211,6 +214,20 @@ const getStyle = (theme: EuiTheme): cytoscape.Stylesheet[] => {
|
|||
'target-arrow-color': theme.eui.euiColorDarkShade,
|
||||
},
|
||||
},
|
||||
...(isTraceExplorerEnabled
|
||||
? [
|
||||
{
|
||||
selector: 'edge.hover',
|
||||
style: {
|
||||
width: 4,
|
||||
'z-index': zIndexEdgeHover,
|
||||
'line-color': theme.eui.euiColorDarkShade,
|
||||
'source-arrow-color': theme.eui.euiColorDarkShade,
|
||||
'target-arrow-color': theme.eui.euiColorDarkShade,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
selector: 'node.hover',
|
||||
style: {
|
||||
|
@ -256,10 +273,11 @@ ${theme.eui.euiColorLightShade}`,
|
|||
});
|
||||
|
||||
export const getCytoscapeOptions = (
|
||||
theme: EuiTheme
|
||||
theme: EuiTheme,
|
||||
isTraceExplorerEnabled: boolean
|
||||
): cytoscape.CytoscapeOptions => ({
|
||||
boxSelectionEnabled: false,
|
||||
maxZoom: 3,
|
||||
minZoom: 0.2,
|
||||
style: getStyle(theme),
|
||||
style: getStyle(theme, isTraceExplorerEnabled),
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { TypeOf } from '@kbn/typed-react-router-config';
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import React from 'react';
|
||||
import { useUiTracker } from '@kbn/observability-plugin/public';
|
||||
import { NodeDataDefinition } from 'cytoscape';
|
||||
import { ContentsProps } from '.';
|
||||
import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
|
@ -27,11 +28,13 @@ const INITIAL_STATE: Partial<BackendReturn> = {
|
|||
};
|
||||
|
||||
export function BackendContents({
|
||||
nodeData,
|
||||
elementData,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
}: ContentsProps) {
|
||||
const nodeData = elementData as NodeDataDefinition;
|
||||
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/service-map',
|
||||
'/services/{serviceName}/service-map'
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import React from 'react';
|
||||
import { useUiTracker } from '@kbn/observability-plugin/public';
|
||||
import { EdgeDataDefinition } from 'cytoscape';
|
||||
import { ContentsProps } from '.';
|
||||
import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
import { TraceSearchType } from '../../../../../common/trace_explorer';
|
||||
import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SPAN_DESTINATION_SERVICE_RESOURCE,
|
||||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
|
||||
export function EdgeContents({ elementData }: ContentsProps) {
|
||||
const edgeData = elementData as EdgeDataDefinition;
|
||||
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/service-map',
|
||||
'/services/{serviceName}/service-map'
|
||||
);
|
||||
|
||||
const apmRouter = useApmRouter();
|
||||
|
||||
const sourceService = edgeData.sourceData['service.name'];
|
||||
|
||||
const trackEvent = useUiTracker();
|
||||
|
||||
let traceQuery: string;
|
||||
|
||||
if (SERVICE_NAME in edgeData.targetData) {
|
||||
traceQuery =
|
||||
`sequence by trace.id\n` +
|
||||
` [ span where service.name == "${sourceService}" and span.destination.service.resource != null ] by span.id\n` +
|
||||
` [ transaction where service.name == "${edgeData.targetData[SERVICE_NAME]}" ] by parent.id`;
|
||||
} else {
|
||||
traceQuery =
|
||||
`sequence by trace.id\n` +
|
||||
` [ transaction where service.name == "${sourceService}" ]\n` +
|
||||
` [ span where service.name == "${sourceService}" and span.destination.service.resource == "${edgeData.targetData[SPAN_DESTINATION_SERVICE_RESOURCE]}" ]`;
|
||||
}
|
||||
|
||||
const url = apmRouter.link('/traces/explorer', {
|
||||
query: {
|
||||
...query,
|
||||
type: TraceSearchType.eql,
|
||||
query: traceQuery,
|
||||
waterfallItemId: '',
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click*/}
|
||||
<EuiButton
|
||||
href={url}
|
||||
fill={true}
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
app: 'apm',
|
||||
metricType: METRIC_TYPE.CLICK,
|
||||
metric: 'service_map_to_trace_explorer',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.apm.serviceMap.viewInTraceExplorer', {
|
||||
defaultMessage: 'Explore traces',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import React, { Fragment } from 'react';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { NodeDataDefinition } from 'cytoscape';
|
||||
import { ContentsProps } from '.';
|
||||
import {
|
||||
SPAN_DESTINATION_SERVICE_RESOURCE,
|
||||
|
@ -26,7 +27,8 @@ const ExternalResourcesList = euiStyled.section`
|
|||
overflow: auto;
|
||||
`;
|
||||
|
||||
export function ExternalsListContents({ nodeData }: ContentsProps) {
|
||||
export function ExternalsListContents({ elementData }: ContentsProps) {
|
||||
const nodeData = elementData as NodeDataDefinition;
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<ExternalResourcesList>
|
||||
|
|
|
@ -28,32 +28,47 @@ import {
|
|||
} from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { Environment } from '../../../../../common/environment_rt';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { useTraceExplorerEnabledSetting } from '../../../../hooks/use_trace_explorer_enabled_setting';
|
||||
import { CytoscapeContext } from '../cytoscape';
|
||||
import { getAnimationOptions, popoverWidth } from '../cytoscape_options';
|
||||
import { BackendContents } from './backend_contents';
|
||||
import { EdgeContents } from './edge_contents';
|
||||
import { ExternalsListContents } from './externals_list_contents';
|
||||
import { ResourceContents } from './resource_contents';
|
||||
import { ServiceContents } from './service_contents';
|
||||
|
||||
function getContentsComponent(selectedNodeData: cytoscape.NodeDataDefinition) {
|
||||
function getContentsComponent(
|
||||
selectedElementData:
|
||||
| cytoscape.NodeDataDefinition
|
||||
| cytoscape.EdgeDataDefinition,
|
||||
isTraceExplorerEnabled: boolean
|
||||
) {
|
||||
if (
|
||||
selectedNodeData.groupedConnections &&
|
||||
Array.isArray(selectedNodeData.groupedConnections)
|
||||
selectedElementData.groupedConnections &&
|
||||
Array.isArray(selectedElementData.groupedConnections)
|
||||
) {
|
||||
return ExternalsListContents;
|
||||
}
|
||||
if (selectedNodeData[SERVICE_NAME]) {
|
||||
if (selectedElementData[SERVICE_NAME]) {
|
||||
return ServiceContents;
|
||||
}
|
||||
if (selectedNodeData[SPAN_TYPE] === 'resource') {
|
||||
if (selectedElementData[SPAN_TYPE] === 'resource') {
|
||||
return ResourceContents;
|
||||
}
|
||||
|
||||
if (
|
||||
isTraceExplorerEnabled &&
|
||||
selectedElementData.source &&
|
||||
selectedElementData.target
|
||||
) {
|
||||
return EdgeContents;
|
||||
}
|
||||
|
||||
return BackendContents;
|
||||
}
|
||||
|
||||
export interface ContentsProps {
|
||||
nodeData: cytoscape.NodeDataDefinition;
|
||||
elementData: cytoscape.NodeDataDefinition | cytoscape.ElementDataDefinition;
|
||||
environment: Environment;
|
||||
kuery: string;
|
||||
start: string;
|
||||
|
@ -78,19 +93,26 @@ export function Popover({
|
|||
}: PopoverProps) {
|
||||
const theme = useTheme();
|
||||
const cy = useContext(CytoscapeContext);
|
||||
const [selectedNode, setSelectedNode] = useState<
|
||||
cytoscape.NodeSingular | undefined
|
||||
const [selectedElement, setSelectedElement] = useState<
|
||||
cytoscape.NodeSingular | cytoscape.EdgeSingular | undefined
|
||||
>(undefined);
|
||||
const deselect = useCallback(() => {
|
||||
if (cy) {
|
||||
cy.elements().unselect();
|
||||
}
|
||||
setSelectedNode(undefined);
|
||||
}, [cy, setSelectedNode]);
|
||||
const renderedHeight = selectedNode?.renderedHeight() ?? 0;
|
||||
const renderedWidth = selectedNode?.renderedWidth() ?? 0;
|
||||
const { x, y } = selectedNode?.renderedPosition() ?? { x: -10000, y: -10000 };
|
||||
const isOpen = !!selectedNode;
|
||||
setSelectedElement(undefined);
|
||||
}, [cy, setSelectedElement]);
|
||||
|
||||
const isTraceExplorerEnabled = useTraceExplorerEnabledSetting();
|
||||
|
||||
const renderedHeight = selectedElement?.renderedHeight() ?? 0;
|
||||
const renderedWidth = selectedElement?.renderedWidth() ?? 0;
|
||||
const box = selectedElement?.renderedBoundingBox({});
|
||||
|
||||
const x = box ? box.x1 + box.w / 2 : -10000;
|
||||
const y = box ? box.y1 + box.h / 2 : -10000;
|
||||
|
||||
const isOpen = !!selectedElement;
|
||||
const triggerStyle: CSSProperties = {
|
||||
background: 'transparent',
|
||||
height: renderedHeight,
|
||||
|
@ -99,20 +121,20 @@ export function Popover({
|
|||
};
|
||||
const trigger = <div style={triggerStyle} />;
|
||||
const zoom = cy?.zoom() ?? 1;
|
||||
const height = selectedNode?.height() ?? 0;
|
||||
const height = selectedElement?.height() ?? 0;
|
||||
const translateY = y - ((zoom + 1) * height) / 4;
|
||||
const popoverStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
transform: `translate(${x}px, ${translateY}px)`,
|
||||
};
|
||||
const selectedNodeData = selectedNode?.data() ?? {};
|
||||
const selectedElementData = selectedElement?.data() ?? {};
|
||||
const popoverRef = useRef<EuiPopover>(null);
|
||||
const selectedNodeId = selectedNodeData.id;
|
||||
const selectedElementId = selectedElementData.id;
|
||||
|
||||
// Set up Cytoscape event handlers
|
||||
useEffect(() => {
|
||||
const selectHandler: cytoscape.EventHandler = (event) => {
|
||||
setSelectedNode(event.target);
|
||||
setSelectedElement(event.target);
|
||||
};
|
||||
|
||||
if (cy) {
|
||||
|
@ -120,6 +142,10 @@ export function Popover({
|
|||
cy.on('unselect', 'node', deselect);
|
||||
cy.on('viewport', deselect);
|
||||
cy.on('drag', 'node', deselect);
|
||||
if (isTraceExplorerEnabled) {
|
||||
cy.on('select', 'edge', selectHandler);
|
||||
cy.on('unselect', 'edge', deselect);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
@ -128,9 +154,11 @@ export function Popover({
|
|||
cy.removeListener('unselect', 'node', deselect);
|
||||
cy.removeListener('viewport', undefined, deselect);
|
||||
cy.removeListener('drag', 'node', deselect);
|
||||
cy.removeListener('select', 'edge', selectHandler);
|
||||
cy.removeListener('unselect', 'edge', deselect);
|
||||
}
|
||||
};
|
||||
}, [cy, deselect]);
|
||||
}, [cy, deselect, isTraceExplorerEnabled]);
|
||||
|
||||
// Handle positioning of popover. This makes it so the popover positions
|
||||
// itself correctly and the arrows are always pointing to where they should.
|
||||
|
@ -146,20 +174,23 @@ export function Popover({
|
|||
if (cy) {
|
||||
cy.animate({
|
||||
...getAnimationOptions(theme),
|
||||
center: { eles: cy.getElementById(selectedNodeId) },
|
||||
center: { eles: cy.getElementById(selectedElementId) },
|
||||
});
|
||||
}
|
||||
},
|
||||
[cy, selectedNodeId, theme]
|
||||
[cy, selectedElementId, theme]
|
||||
);
|
||||
|
||||
const isAlreadyFocused = focusedServiceName === selectedNodeId;
|
||||
const isAlreadyFocused = focusedServiceName === selectedElementId;
|
||||
|
||||
const onFocusClick = isAlreadyFocused
|
||||
? centerSelectedNode
|
||||
: (_event: MouseEvent<HTMLAnchorElement>) => deselect();
|
||||
|
||||
const ContentsComponent = getContentsComponent(selectedNodeData);
|
||||
const ContentsComponent = getContentsComponent(
|
||||
selectedElementData,
|
||||
isTraceExplorerEnabled
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
|
@ -179,14 +210,14 @@ export function Popover({
|
|||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h3 style={{ wordBreak: 'break-all' }}>
|
||||
{selectedNodeData.label ?? selectedNodeId}
|
||||
{selectedElementData.label ?? selectedElementId}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
</EuiFlexItem>
|
||||
<ContentsComponent
|
||||
onFocusClick={onFocusClick}
|
||||
nodeData={selectedNodeData}
|
||||
elementData={selectedElementData}
|
||||
environment={environment}
|
||||
kuery={kuery}
|
||||
start={start}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { NodeDataDefinition } from 'cytoscape';
|
||||
import type { ContentsProps } from '.';
|
||||
import {
|
||||
SPAN_SUBTYPE,
|
||||
|
@ -28,7 +29,8 @@ const SubduedDescriptionListTitle = euiStyled(EuiDescriptionListTitle)`
|
|||
}
|
||||
`;
|
||||
|
||||
export function ResourceContents({ nodeData }: ContentsProps) {
|
||||
export function ResourceContents({ elementData }: ContentsProps) {
|
||||
const nodeData = elementData as NodeDataDefinition;
|
||||
const subtype = nodeData[SPAN_SUBTYPE];
|
||||
const type = nodeData[SPAN_TYPE];
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { NodeDataDefinition } from 'cytoscape';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import type { ContentsProps } from '.';
|
||||
import { useApmRouter } from '../../../../hooks/use_apm_router';
|
||||
|
@ -34,10 +35,11 @@ const INITIAL_STATE: ServiceNodeReturn = {
|
|||
|
||||
export function ServiceContents({
|
||||
onFocusClick,
|
||||
nodeData,
|
||||
elementData,
|
||||
environment,
|
||||
kuery,
|
||||
}: ContentsProps) {
|
||||
const nodeData = elementData as NodeDataDefinition;
|
||||
const apmRouter = useApmRouter();
|
||||
|
||||
const { query } = useApmParams('/*');
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { SearchBar } from '../../shared/search_bar';
|
||||
import { TraceList } from './trace_list';
|
||||
import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher';
|
||||
import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
|
||||
|
||||
export function TopTracesOverview() {
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
} = useApmParams('/traces');
|
||||
const { fallbackToTransactions } = useFallbackToTransactionsFetcher({
|
||||
kuery,
|
||||
});
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const response = useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi('GET /internal/apm/traces', {
|
||||
params: {
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[environment, kuery, start, end]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar />
|
||||
|
||||
{fallbackToTransactions && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AggregatedTransactionsBadge />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
<TraceList response={response} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -16,6 +16,7 @@ import {
|
|||
asTransactionRate,
|
||||
} from '../../../../common/utils/formatters';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { FetcherResult, FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { truncate } from '../../../utils/style';
|
||||
import { EmptyMessage } from '../../shared/empty_message';
|
||||
|
@ -26,19 +27,17 @@ import { ServiceLink } from '../../shared/service_link';
|
|||
import { TruncateWithTooltip } from '../../shared/truncate_with_tooltip';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n';
|
||||
|
||||
type TraceGroup = APIReturnType<'GET /internal/apm/traces'>['items'][0];
|
||||
|
||||
const StyledTransactionLink = euiStyled(TransactionDetailLink)`
|
||||
font-size: ${({ theme }) => theme.eui.euiFontSizeS};
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
items: TraceGroup[];
|
||||
isLoading: boolean;
|
||||
isFailure: boolean;
|
||||
response: FetcherResult<APIReturnType<'GET /internal/apm/traces'>>;
|
||||
}
|
||||
|
||||
type TraceGroup = Required<Props['response']>['data']['items'][number];
|
||||
|
||||
export function getTraceListColumns({
|
||||
query,
|
||||
}: {
|
||||
|
@ -153,7 +152,9 @@ const noItemsMessage = (
|
|||
/>
|
||||
);
|
||||
|
||||
export function TraceList({ items = [], isLoading, isFailure }: Props) {
|
||||
export function TraceList({ response }: Props) {
|
||||
const { data: { items } = { items: [] }, status } = response;
|
||||
|
||||
const { query } = useApmParams('/traces');
|
||||
|
||||
const traceListColumns = useMemo(
|
||||
|
@ -162,8 +163,8 @@ export function TraceList({ items = [], isLoading, isFailure }: Props) {
|
|||
);
|
||||
return (
|
||||
<ManagedTable
|
||||
isLoading={isLoading}
|
||||
error={isFailure}
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
error={status === FETCH_STATUS.FAILURE}
|
||||
columns={traceListColumns}
|
||||
items={items}
|
||||
initialSortField="impact"
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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 } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
TraceSearchQuery,
|
||||
TraceSearchType,
|
||||
} from '../../../../common/trace_explorer';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { ApmDatePicker } from '../../shared/date_picker/apm_date_picker';
|
||||
import { fromQuery, toQuery, push } from '../../shared/links/url_helpers';
|
||||
import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
import { TraceSearchBox } from './trace_search_box';
|
||||
|
||||
export function TraceExplorer() {
|
||||
const [query, setQuery] = useState<TraceSearchQuery>({
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
});
|
||||
|
||||
const {
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
traceId,
|
||||
transactionId,
|
||||
waterfallItemId,
|
||||
detailTab,
|
||||
},
|
||||
} = useApmParams('/traces/explorer');
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
setQuery({
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
});
|
||||
}, [queryFromUrlParams, typeFromUrlParams]);
|
||||
|
||||
const { start, end } = useTimeRange({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
});
|
||||
|
||||
const { data: traceSamplesData, status: traceSamplesStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/traces/find', {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[start, end, environment, queryFromUrlParams, typeFromUrlParams]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const nextSample = traceSamplesData?.samples[0];
|
||||
const nextWaterfallItemId = '';
|
||||
history.replace({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
traceId: nextSample?.traceId ?? '',
|
||||
transactionId: nextSample?.transactionId,
|
||||
waterfallItemId: nextWaterfallItemId,
|
||||
}),
|
||||
});
|
||||
}, [traceSamplesData, history]);
|
||||
|
||||
const { waterfall, status: waterfallStatus } = useWaterfallFetcher({
|
||||
traceId,
|
||||
transactionId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const isLoading =
|
||||
traceSamplesStatus === FETCH_STATUS.LOADING ||
|
||||
waterfallStatus === FETCH_STATUS.LOADING;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow>
|
||||
<TraceSearchBox
|
||||
query={query}
|
||||
error={false}
|
||||
loading={false}
|
||||
onQueryCommit={() => {
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
query: query.query,
|
||||
type: query.type,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
onQueryChange={(nextQuery) => {
|
||||
setQuery(nextQuery);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ApmDatePicker />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<WaterfallWithSummary
|
||||
environment={environment}
|
||||
isLoading={isLoading}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, {
|
||||
query: {
|
||||
traceId: sample.traceId,
|
||||
transactionId: sample.transactionId,
|
||||
waterfallItemId: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onTabClick={(nextDetailTab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: nextDetailTab,
|
||||
},
|
||||
});
|
||||
}}
|
||||
traceSamples={traceSamplesData?.samples ?? []}
|
||||
waterfall={waterfall}
|
||||
detailTab={detailTab}
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={waterfall.entryWaterfallTransaction?.doc.service.name}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiText,
|
||||
EuiSelect,
|
||||
EuiSelectOption,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
|
||||
import React from 'react';
|
||||
import {
|
||||
TraceSearchQuery,
|
||||
TraceSearchType,
|
||||
} from '../../../../../common/trace_explorer';
|
||||
import { useStaticDataView } from '../../../../hooks/use_static_data_view';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { EQLCodeEditorSuggestionType } from '../../../shared/eql_code_editor/constants';
|
||||
import { LazilyLoadedEQLCodeEditor } from '../../../shared/eql_code_editor/lazily_loaded_code_editor';
|
||||
|
||||
interface Props {
|
||||
query: TraceSearchQuery;
|
||||
message?: string;
|
||||
error: boolean;
|
||||
onQueryChange: (query: TraceSearchQuery) => void;
|
||||
onQueryCommit: () => void;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const options: EuiSelectOption[] = [
|
||||
{
|
||||
value: TraceSearchType.kql,
|
||||
text: i18n.translate('xpack.apm.traceSearchBox.traceSearchTypeKql', {
|
||||
defaultMessage: 'KQL',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: TraceSearchType.eql,
|
||||
text: i18n.translate('xpack.apm.traceSearchBox.traceSearchTypeEql', {
|
||||
defaultMessage: 'EQL',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export function TraceSearchBox({
|
||||
query,
|
||||
onQueryChange,
|
||||
onQueryCommit,
|
||||
message,
|
||||
error,
|
||||
loading,
|
||||
}: Props) {
|
||||
const { unifiedSearch } = useApmPluginContext();
|
||||
const { value: dataView } = useStaticDataView();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiFlexItem grow>
|
||||
{query.type === TraceSearchType.eql ? (
|
||||
<LazilyLoadedEQLCodeEditor
|
||||
value={query.query}
|
||||
onChange={(value) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
query: value,
|
||||
});
|
||||
}}
|
||||
onBlur={() => {
|
||||
onQueryCommit();
|
||||
}}
|
||||
getSuggestions={async (request) => {
|
||||
switch (request.type) {
|
||||
case EQLCodeEditorSuggestionType.EventType:
|
||||
return ['transaction', 'span', 'error'];
|
||||
|
||||
case EQLCodeEditorSuggestionType.Field:
|
||||
return (
|
||||
dataView?.fields.map((field) => field.name) ?? []
|
||||
);
|
||||
|
||||
case EQLCodeEditorSuggestionType.Value:
|
||||
const field = dataView?.getFieldByName(request.field);
|
||||
|
||||
if (!dataView || !field) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const suggestions: string[] =
|
||||
await unifiedSearch.autocomplete.getValueSuggestions(
|
||||
{
|
||||
field,
|
||||
indexPattern: dataView,
|
||||
query: request.value,
|
||||
useTimeRange: true,
|
||||
method: 'terms_agg',
|
||||
}
|
||||
);
|
||||
|
||||
return suggestions.slice(0, 15);
|
||||
}
|
||||
}}
|
||||
width="100%"
|
||||
height="100px"
|
||||
/>
|
||||
) : (
|
||||
<form>
|
||||
<QueryStringInput
|
||||
disableLanguageSwitcher
|
||||
indexPatterns={dataView ? [dataView] : []}
|
||||
query={{
|
||||
query: query.query,
|
||||
language: 'kuery',
|
||||
}}
|
||||
onSubmit={() => {
|
||||
onQueryCommit();
|
||||
}}
|
||||
disableAutoFocus
|
||||
submitOnBlur
|
||||
isClearable
|
||||
onChange={(e) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
query: String(e.query ?? ''),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
id="select-query-language"
|
||||
value={query.type}
|
||||
onChange={(e) => {
|
||||
onQueryChange({
|
||||
query: '',
|
||||
type: e.target.value as TraceSearchType,
|
||||
});
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiText color={error ? 'danger' : 'subdued'} size="xs">
|
||||
{message}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
isLoading={loading}
|
||||
onClick={() => {
|
||||
onQueryCommit();
|
||||
}}
|
||||
iconType="search"
|
||||
>
|
||||
{i18n.translate('xpack.apm.traceSearchBox.refreshButton', {
|
||||
defaultMessage: 'Search',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -4,69 +4,81 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { SearchBar } from '../../shared/search_bar';
|
||||
import { TraceList } from './trace_list';
|
||||
import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher';
|
||||
import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { useProgressiveFetcher } from '../../../hooks/use_progressive_fetcher';
|
||||
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
|
||||
import { TraceSearchType } from '../../../../common/trace_explorer';
|
||||
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import { useTraceExplorerEnabledSetting } from '../../../hooks/use_trace_explorer_enabled_setting';
|
||||
|
||||
type TracesAPIResponse = APIReturnType<'GET /internal/apm/traces'>;
|
||||
const DEFAULT_RESPONSE: TracesAPIResponse = {
|
||||
items: [],
|
||||
};
|
||||
export function TraceOverview({ children }: { children: React.ReactElement }) {
|
||||
const isTraceExplorerEnabled = useTraceExplorerEnabledSetting();
|
||||
|
||||
export function TraceOverview() {
|
||||
const {
|
||||
query: { environment, kuery, rangeFrom, rangeTo },
|
||||
} = useApmParams('/traces');
|
||||
const { fallbackToTransactions } = useFallbackToTransactionsFetcher({
|
||||
kuery,
|
||||
const router = useApmRouter();
|
||||
|
||||
const { query } = useApmParams('/traces');
|
||||
|
||||
const routePath = useApmRoutePath();
|
||||
|
||||
if (!isTraceExplorerEnabled) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const explorerLink = router.link('/traces/explorer', {
|
||||
query: {
|
||||
comparisonEnabled: query.comparisonEnabled,
|
||||
environment: query.environment,
|
||||
kuery: query.kuery,
|
||||
rangeFrom: query.rangeFrom,
|
||||
rangeTo: query.rangeTo,
|
||||
offset: query.offset,
|
||||
refreshInterval: query.refreshInterval,
|
||||
refreshPaused: query.refreshPaused,
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
waterfallItemId: '',
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
});
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { status, data = DEFAULT_RESPONSE } = useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
if (start && end) {
|
||||
return callApmApi('GET /internal/apm/traces', {
|
||||
params: {
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const topTracesLink = router.link('/traces', {
|
||||
query: {
|
||||
comparisonEnabled: query.comparisonEnabled,
|
||||
environment: query.environment,
|
||||
kuery: query.kuery,
|
||||
rangeFrom: query.rangeFrom,
|
||||
rangeTo: query.rangeTo,
|
||||
offset: query.offset,
|
||||
refreshInterval: query.refreshInterval,
|
||||
refreshPaused: query.refreshPaused,
|
||||
},
|
||||
[environment, kuery, start, end]
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar />
|
||||
|
||||
{fallbackToTransactions && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AggregatedTransactionsBadge />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
<TraceList
|
||||
items={data.items}
|
||||
isLoading={status === FETCH_STATUS.LOADING}
|
||||
isFailure={status === FETCH_STATUS.FAILURE}
|
||||
/>
|
||||
</>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTabs>
|
||||
<EuiTab href={topTracesLink} isSelected={routePath === '/traces'}>
|
||||
{i18n.translate('xpack.apm.traceOverview.topTracesTab', {
|
||||
defaultMessage: 'Top traces',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
href={explorerLink}
|
||||
isSelected={routePath === '/traces/explorer'}
|
||||
>
|
||||
{i18n.translate('xpack.apm.traceOverview.traceExplorerTab', {
|
||||
defaultMessage: 'Explorer',
|
||||
})}
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { BrushEndListener, XYBrushEvent } from '@elastic/charts';
|
||||
import {
|
||||
EuiBadge,
|
||||
|
@ -16,6 +15,8 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -32,9 +33,14 @@ import type { TabContentProps } from '../types';
|
|||
import { useWaterfallFetcher } from '../use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../waterfall_with_summary';
|
||||
|
||||
import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { HeightRetainer } from '../../../shared/height_retainer';
|
||||
import { fromQuery, toQuery } from '../../../shared/links/url_helpers';
|
||||
import { ChartTitleToolTip } from '../../correlations/chart_title_tool_tip';
|
||||
import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data';
|
||||
import { TransactionTab } from '../waterfall_with_summary/transaction_tabs';
|
||||
|
||||
// Enforce min height so it's consistent across all tabs on the same level
|
||||
// to prevent "flickering" behavior
|
||||
|
@ -70,8 +76,28 @@ export function TransactionDistribution({
|
|||
traceSamplesStatus,
|
||||
}: TransactionDistributionProps) {
|
||||
const { urlParams } = useLegacyUrlParams();
|
||||
const { waterfall, status: waterfallStatus } = useWaterfallFetcher();
|
||||
const { traceId, transactionId } = urlParams;
|
||||
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const history = useHistory();
|
||||
const { waterfall, status: waterfallStatus } = useWaterfallFetcher({
|
||||
traceId,
|
||||
transactionId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
const { waterfallItemId, detailTab } = urlParams;
|
||||
|
||||
const {
|
||||
query: { environment },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
const { serviceName } = useApmServiceContext();
|
||||
const isLoading =
|
||||
waterfallStatus === FETCH_STATUS.LOADING ||
|
||||
traceSamplesStatus === FETCH_STATUS.LOADING;
|
||||
|
@ -193,7 +219,29 @@ export function TransactionDistribution({
|
|||
|
||||
<EuiSpacer size="s" />
|
||||
<WaterfallWithSummary
|
||||
urlParams={urlParams}
|
||||
environment={environment}
|
||||
onSampleClick={(sample) => {
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
transactionId: sample.transactionId,
|
||||
traceId: sample.traceId,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
onTabClick={(tab) => {
|
||||
history.replace({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
detailTab: tab,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
detailTab={detailTab as TransactionTab | undefined}
|
||||
waterfall={waterfall}
|
||||
isLoading={isLoading}
|
||||
traceSamples={traceSamples}
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { getWaterfall } from './waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
|
@ -20,16 +17,17 @@ const INITIAL_DATA: APIReturnType<'GET /internal/apm/traces/{traceId}'> = {
|
|||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
};
|
||||
|
||||
export function useWaterfallFetcher() {
|
||||
const { urlParams } = useLegacyUrlParams();
|
||||
const { traceId, transactionId } = urlParams;
|
||||
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
export function useWaterfallFetcher({
|
||||
traceId,
|
||||
transactionId,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
traceId?: string;
|
||||
transactionId?: string;
|
||||
start: string;
|
||||
end: string;
|
||||
}) {
|
||||
const {
|
||||
data = INITIAL_DATA,
|
||||
status,
|
||||
|
|
|
@ -15,38 +15,40 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import type { ApmUrlParams } from '../../../../context/url_params_context/types';
|
||||
import { fromQuery, toQuery } from '../../../shared/links/url_helpers';
|
||||
import { LoadingStatePrompt } from '../../../shared/loading_state_prompt';
|
||||
import { TransactionSummary } from '../../../shared/summary/transaction_summary';
|
||||
import { TransactionActionMenu } from '../../../shared/transaction_action_menu/transaction_action_menu';
|
||||
import type { TraceSample } from '../../../../hooks/use_transaction_trace_samples_fetcher';
|
||||
import { MaybeViewTraceLink } from './maybe_view_trace_link';
|
||||
import { TransactionTabs } from './transaction_tabs';
|
||||
import { TransactionTab, TransactionTabs } from './transaction_tabs';
|
||||
import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { Environment } from '../../../../../common/environment_rt';
|
||||
|
||||
interface Props {
|
||||
urlParams: ApmUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
isLoading: boolean;
|
||||
traceSamples: TraceSample[];
|
||||
environment: Environment;
|
||||
onSampleClick: (sample: { transactionId: string; traceId: string }) => void;
|
||||
onTabClick: (tab: string) => void;
|
||||
serviceName?: string;
|
||||
waterfallItemId?: string;
|
||||
detailTab?: TransactionTab;
|
||||
}
|
||||
|
||||
export function WaterfallWithSummary({
|
||||
urlParams,
|
||||
waterfall,
|
||||
isLoading,
|
||||
traceSamples,
|
||||
environment,
|
||||
onSampleClick,
|
||||
onTabClick,
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
detailTab,
|
||||
}: Props) {
|
||||
const history = useHistory();
|
||||
const [sampleActivePage, setSampleActivePage] = useState(0);
|
||||
|
||||
const {
|
||||
query: { environment },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
useEffect(() => {
|
||||
setSampleActivePage(0);
|
||||
}, [traceSamples]);
|
||||
|
@ -54,14 +56,7 @@ export function WaterfallWithSummary({
|
|||
const goToSample = (index: number) => {
|
||||
setSampleActivePage(index);
|
||||
const sample = traceSamples[index];
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
transactionId: sample.transactionId,
|
||||
traceId: sample.traceId,
|
||||
}),
|
||||
});
|
||||
onSampleClick(sample);
|
||||
};
|
||||
|
||||
const { entryWaterfallTransaction } = waterfall;
|
||||
|
@ -137,7 +132,10 @@ export function WaterfallWithSummary({
|
|||
<EuiSpacer size="s" />
|
||||
<TransactionTabs
|
||||
transaction={entryTransaction}
|
||||
urlParams={urlParams}
|
||||
detailTab={detailTab}
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
onTabClick={onTabClick}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -13,7 +13,8 @@ import { Transaction as ITransaction } from '../../../../../typings/es_schemas/u
|
|||
import { TransactionDetailLink } from '../../../shared/links/apm/transaction_detail_link';
|
||||
import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { Environment } from '../../../../../common/environment_rt';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useAnyOfApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
|
||||
|
||||
function FullTraceButton({
|
||||
isLoading,
|
||||
|
@ -48,8 +49,16 @@ export function MaybeViewTraceLink({
|
|||
environment: Environment;
|
||||
}) {
|
||||
const {
|
||||
query: { latencyAggregationType, comparisonEnabled, offset },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
query,
|
||||
query: { comparisonEnabled, offset },
|
||||
} = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
);
|
||||
|
||||
const latencyAggregationType =
|
||||
('latencyAggregationType' in query && query.latencyAggregationType) ||
|
||||
LatencyAggregationType.avg;
|
||||
|
||||
if (isLoading || !transaction) {
|
||||
return <FullTraceButton isLoading={isLoading} />;
|
||||
|
|
|
@ -7,27 +7,32 @@
|
|||
|
||||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { LogStream } from '@kbn/infra-plugin/public';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/transaction';
|
||||
import type { ApmUrlParams } from '../../../../context/url_params_context/types';
|
||||
import { fromQuery, toQuery } from '../../../shared/links/url_helpers';
|
||||
import { TransactionMetadata } from '../../../shared/metadata_table/transaction_metadata';
|
||||
import { WaterfallContainer } from './waterfall_container';
|
||||
import { IWaterfall } from './waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
urlParams: ApmUrlParams;
|
||||
waterfall: IWaterfall;
|
||||
detailTab?: TransactionTab;
|
||||
serviceName?: string;
|
||||
waterfallItemId?: string;
|
||||
onTabClick: (tab: TransactionTab) => void;
|
||||
}
|
||||
|
||||
export function TransactionTabs({ transaction, urlParams, waterfall }: Props) {
|
||||
const history = useHistory();
|
||||
export function TransactionTabs({
|
||||
transaction,
|
||||
waterfall,
|
||||
detailTab,
|
||||
waterfallItemId,
|
||||
serviceName,
|
||||
onTabClick,
|
||||
}: Props) {
|
||||
const tabs = [timelineTab, metadataTab, logsTab];
|
||||
const currentTab =
|
||||
tabs.find(({ key }) => key === urlParams.detailTab) ?? timelineTab;
|
||||
const currentTab = tabs.find(({ key }) => key === detailTab) ?? timelineTab;
|
||||
const TabContent = currentTab.component;
|
||||
|
||||
return (
|
||||
|
@ -37,13 +42,7 @@ export function TransactionTabs({ transaction, urlParams, waterfall }: Props) {
|
|||
return (
|
||||
<EuiTab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
detailTab: key,
|
||||
}),
|
||||
});
|
||||
onTabClick(key);
|
||||
}}
|
||||
isSelected={currentTab.key === key}
|
||||
key={key}
|
||||
|
@ -57,7 +56,8 @@ export function TransactionTabs({ transaction, urlParams, waterfall }: Props) {
|
|||
<EuiSpacer />
|
||||
|
||||
<TabContent
|
||||
urlParams={urlParams}
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={serviceName}
|
||||
waterfall={waterfall}
|
||||
transaction={transaction}
|
||||
/>
|
||||
|
@ -65,8 +65,14 @@ export function TransactionTabs({ transaction, urlParams, waterfall }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export enum TransactionTab {
|
||||
timeline = 'timeline',
|
||||
metadata = 'metadata',
|
||||
logs = 'logs',
|
||||
}
|
||||
|
||||
const timelineTab = {
|
||||
key: 'timeline',
|
||||
key: TransactionTab.timeline,
|
||||
label: i18n.translate('xpack.apm.propertiesTable.tabs.timelineLabel', {
|
||||
defaultMessage: 'Timeline',
|
||||
}),
|
||||
|
@ -74,7 +80,7 @@ const timelineTab = {
|
|||
};
|
||||
|
||||
const metadataTab = {
|
||||
key: 'metadata',
|
||||
key: TransactionTab.metadata,
|
||||
label: i18n.translate('xpack.apm.propertiesTable.tabs.metadataLabel', {
|
||||
defaultMessage: 'Metadata',
|
||||
}),
|
||||
|
@ -82,7 +88,7 @@ const metadataTab = {
|
|||
};
|
||||
|
||||
const logsTab = {
|
||||
key: 'logs',
|
||||
key: TransactionTab.logs,
|
||||
label: i18n.translate('xpack.apm.propertiesTable.tabs.logsLabel', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
|
@ -90,13 +96,21 @@ const logsTab = {
|
|||
};
|
||||
|
||||
function TimelineTabContent({
|
||||
urlParams,
|
||||
waterfall,
|
||||
waterfallItemId,
|
||||
serviceName,
|
||||
}: {
|
||||
urlParams: ApmUrlParams;
|
||||
waterfallItemId?: string;
|
||||
serviceName?: string;
|
||||
waterfall: IWaterfall;
|
||||
}) {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
return (
|
||||
<WaterfallContainer
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={serviceName}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MetadataTabContent({ transaction }: { transaction: Transaction }) {
|
||||
|
|
|
@ -7,23 +7,24 @@
|
|||
|
||||
import React from 'react';
|
||||
import { keyBy } from 'lodash';
|
||||
import type { ApmUrlParams } from '../../../../../context/url_params_context/types';
|
||||
import {
|
||||
IWaterfall,
|
||||
WaterfallLegendType,
|
||||
} from './waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { Waterfall } from './waterfall';
|
||||
import { WaterfallLegends } from './waterfall_legends';
|
||||
import { useApmServiceContext } from '../../../../../context/apm_service/use_apm_service_context';
|
||||
|
||||
interface Props {
|
||||
urlParams: ApmUrlParams;
|
||||
waterfallItemId?: string;
|
||||
serviceName?: string;
|
||||
waterfall: IWaterfall;
|
||||
}
|
||||
|
||||
export function WaterfallContainer({ urlParams, waterfall }: Props) {
|
||||
const { serviceName } = useApmServiceContext();
|
||||
|
||||
export function WaterfallContainer({
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
waterfall,
|
||||
}: Props) {
|
||||
if (!waterfall) {
|
||||
return null;
|
||||
}
|
||||
|
@ -75,10 +76,7 @@ export function WaterfallContainer({ urlParams, waterfall }: Props) {
|
|||
return (
|
||||
<div>
|
||||
<WaterfallLegends legends={legendsWithFallbackLabel} type={colorBy} />
|
||||
<Waterfall
|
||||
waterfallItemId={urlParams.waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
<Waterfall waterfallItemId={waterfallItemId} waterfall={waterfall} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ import {
|
|||
TRANSACTION_NAME,
|
||||
} from '../../../../../../../common/elasticsearch_fieldnames';
|
||||
import { getNextEnvironmentUrlParam } from '../../../../../../../common/environment_filter_values';
|
||||
import { LatencyAggregationType } from '../../../../../../../common/latency_aggregation_types';
|
||||
import { Transaction } from '../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import { useApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
import { TransactionDetailLink } from '../../../../../shared/links/apm/transaction_detail_link';
|
||||
import { ServiceLink } from '../../../../../shared/service_link';
|
||||
import { StickyProperties } from '../../../../../shared/sticky_properties';
|
||||
|
@ -23,9 +24,17 @@ interface Props {
|
|||
}
|
||||
|
||||
export function FlyoutTopLevelProperties({ transaction }: Props) {
|
||||
const { query } = useApmParams('/services/{serviceName}/transactions/view');
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
);
|
||||
|
||||
const { latencyAggregationType, comparisonEnabled, offset } = query;
|
||||
const latencyAggregationType =
|
||||
('latencyAggregationType' in query && query.latencyAggregationType) ||
|
||||
LatencyAggregationType.avg;
|
||||
const serviceGroup = ('serviceGroup' in query && query.serviceGroup) || '';
|
||||
|
||||
const { comparisonEnabled, offset } = query;
|
||||
|
||||
if (!transaction) {
|
||||
return null;
|
||||
|
@ -45,7 +54,7 @@ export function FlyoutTopLevelProperties({ transaction }: Props) {
|
|||
val: (
|
||||
<ServiceLink
|
||||
agentName={transaction.agent.name}
|
||||
query={{ ...query, environment: nextEnvironment }}
|
||||
query={{ ...query, serviceGroup, environment: nextEnvironment }}
|
||||
serviceName={transaction.service.name}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -18,11 +18,12 @@ import { getNextEnvironmentUrlParam } from '../../../../../../../../common/envir
|
|||
import { NOT_AVAILABLE_LABEL } from '../../../../../../../../common/i18n';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import { useApmParams } from '../../../../../../../hooks/use_apm_params';
|
||||
import { useAnyOfApmParams } from '../../../../../../../hooks/use_apm_params';
|
||||
import { BackendLink } from '../../../../../../shared/backend_link';
|
||||
import { TransactionDetailLink } from '../../../../../../shared/links/apm/transaction_detail_link';
|
||||
import { ServiceLink } from '../../../../../../shared/service_link';
|
||||
import { StickyProperties } from '../../../../../../shared/sticky_properties';
|
||||
import { LatencyAggregationType } from '../../../../../../../../common/latency_aggregation_types';
|
||||
|
||||
interface Props {
|
||||
span: Span;
|
||||
|
@ -30,9 +31,17 @@ interface Props {
|
|||
}
|
||||
|
||||
export function StickySpanProperties({ span, transaction }: Props) {
|
||||
const { query } = useApmParams('/services/{serviceName}/transactions/view');
|
||||
const { environment, latencyAggregationType, comparisonEnabled, offset } =
|
||||
query;
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
);
|
||||
const { environment, comparisonEnabled, offset } = query;
|
||||
|
||||
const latencyAggregationType =
|
||||
('latencyAggregationType' in query && query.latencyAggregationType) ||
|
||||
LatencyAggregationType.avg;
|
||||
|
||||
const serviceGroup = ('serviceGroup' in query && query.serviceGroup) || '';
|
||||
|
||||
const trackEvent = useUiTracker();
|
||||
|
||||
|
@ -56,6 +65,7 @@ export function StickySpanProperties({ span, transaction }: Props) {
|
|||
agentName={transaction.agent.name}
|
||||
query={{
|
||||
...query,
|
||||
serviceGroup,
|
||||
environment: nextEnvironment,
|
||||
}}
|
||||
serviceName={transaction.service.name}
|
||||
|
|
|
@ -24,7 +24,7 @@ import { ColdStartBadge } from './badge/cold_start_badge';
|
|||
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { FailureBadge } from './failure_badge';
|
||||
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
|
||||
import { useApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
import { useAnyOfApmParams } from '../../../../../../hooks/use_apm_params';
|
||||
|
||||
type ItemType = 'transaction' | 'span' | 'error';
|
||||
|
||||
|
@ -258,12 +258,16 @@ function RelatedErrors({
|
|||
}) {
|
||||
const apmRouter = useApmRouter();
|
||||
const theme = useTheme();
|
||||
const { query } = useApmParams('/services/{serviceName}/transactions/view');
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
);
|
||||
|
||||
const href = apmRouter.link(`/services/{serviceName}/errors`, {
|
||||
path: { serviceName: item.doc.service.name },
|
||||
query: {
|
||||
...query,
|
||||
serviceGroup: '',
|
||||
kuery: `${TRACE_ID} : "${item.doc.trace.id}" and ${TRANSACTION_ID} : "${item.doc.transaction?.id}"`,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
simpleTrace,
|
||||
traceChildStartBeforeParent,
|
||||
traceWithErrors,
|
||||
urlParams as testUrlParams,
|
||||
} from './waterfall_container.stories.data';
|
||||
import type { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
|
||||
|
||||
|
@ -50,48 +49,87 @@ const stories: Meta<Args> = {
|
|||
};
|
||||
export default stories;
|
||||
|
||||
export const Example: Story<Args> = ({ urlParams, waterfall }) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
export const Example: Story<Args> = ({
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
waterfall,
|
||||
}) => {
|
||||
return (
|
||||
<WaterfallContainer
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Example.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(simpleTrace, '975c8d5bfd1dd20b'),
|
||||
};
|
||||
|
||||
export const WithErrors: Story<Args> = ({ urlParams, waterfall }) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
export const WithErrors: Story<Args> = ({
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
waterfall,
|
||||
}) => {
|
||||
return (
|
||||
<WaterfallContainer
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
WithErrors.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(traceWithErrors, '975c8d5bfd1dd20b'),
|
||||
};
|
||||
|
||||
export const ChildStartsBeforeParent: Story<Args> = ({
|
||||
urlParams,
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
waterfall,
|
||||
}) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
return (
|
||||
<WaterfallContainer
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ChildStartsBeforeParent.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(traceChildStartBeforeParent, '975c8d5bfd1dd20b'),
|
||||
};
|
||||
|
||||
export const InferredSpans: Story<Args> = ({ urlParams, waterfall }) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
export const InferredSpans: Story<Args> = ({
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
waterfall,
|
||||
}) => {
|
||||
return (
|
||||
<WaterfallContainer
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
InferredSpans.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(inferredSpans, 'f2387d37260d00bd'),
|
||||
};
|
||||
|
||||
export const ManyChildrenWithSameLength: Story<Args> = ({
|
||||
urlParams,
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
waterfall,
|
||||
}) => {
|
||||
return <WaterfallContainer urlParams={urlParams} waterfall={waterfall} />;
|
||||
return (
|
||||
<WaterfallContainer
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
waterfall={waterfall}
|
||||
/>
|
||||
);
|
||||
};
|
||||
ManyChildrenWithSameLength.args = {
|
||||
urlParams: testUrlParams,
|
||||
waterfall: getWaterfall(manyChildrenWithSameLength, '9a7f717439921d39'),
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
HeaderMenuPortal,
|
||||
InspectorContextProvider,
|
||||
} from '@kbn/observability-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { ScrollToTopOnPathChange } from '../app/main/scroll_to_top_on_path_change';
|
||||
import { AnomalyDetectionJobsContextProvider } from '../../context/anomaly_detection_jobs/anomaly_detection_jobs_context';
|
||||
import {
|
||||
|
@ -39,6 +40,8 @@ import { TrackPageview } from './track_pageview';
|
|||
import { RedirectWithDefaultEnvironment } from '../shared/redirect_with_default_environment';
|
||||
import { RedirectWithOffset } from '../shared/redirect_with_offset';
|
||||
|
||||
const storage = new Storage(localStorage);
|
||||
|
||||
export function ApmAppRoot({
|
||||
apmPluginContextValue,
|
||||
pluginsStart,
|
||||
|
@ -58,7 +61,7 @@ export function ApmAppRoot({
|
|||
role="main"
|
||||
>
|
||||
<ApmPluginContext.Provider value={apmPluginContextValue}>
|
||||
<KibanaContextProvider services={{ ...core, ...pluginsStart }}>
|
||||
<KibanaContextProvider services={{ ...core, ...pluginsStart, storage }}>
|
||||
<i18nCore.Context>
|
||||
<TimeRangeIdContextProvider>
|
||||
<RouterProvider history={history} router={apmRouter as any}>
|
||||
|
|
|
@ -5,40 +5,49 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Outlet } from '@kbn/typed-react-router-config';
|
||||
import { Outlet, Route } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { toBooleanRt } from '@kbn/io-ts-utils';
|
||||
import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
|
||||
import { environmentRt } from '../../../../common/environment_rt';
|
||||
import { TraceSearchType } from '../../../../common/trace_explorer';
|
||||
import { BackendDetailOverview } from '../../app/backend_detail_overview';
|
||||
import { BackendInventory } from '../../app/backend_inventory';
|
||||
import { Breadcrumb } from '../../app/breadcrumb';
|
||||
import { ServiceInventory } from '../../app/service_inventory';
|
||||
import { ServiceMapHome } from '../../app/service_map';
|
||||
import { TraceOverview } from '../../app/trace_overview';
|
||||
import { TraceExplorer } from '../../app/trace_explorer';
|
||||
import { TopTracesOverview } from '../../app/top_traces_overview';
|
||||
import { ApmMainTemplate } from '../templates/apm_main_template';
|
||||
import { RedirectToBackendOverviewRouteView } from './redirect_to_backend_overview_route_view';
|
||||
import { ServiceGroupTemplate } from '../templates/service_group_template';
|
||||
import { ServiceGroupsRedirect } from '../service_groups_redirect';
|
||||
import { RedirectTo } from '../redirect_to';
|
||||
import { offsetRt } from '../../../../common/offset_rt';
|
||||
import { TransactionTab } from '../../app/transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
|
||||
function page<TPath extends string>({
|
||||
function page<
|
||||
TPath extends string,
|
||||
TChildren extends Record<string, Route> | undefined = undefined
|
||||
>({
|
||||
path,
|
||||
element,
|
||||
children,
|
||||
title,
|
||||
showServiceGroupSaveButton = false,
|
||||
}: {
|
||||
path: TPath;
|
||||
element: React.ReactElement<any, any>;
|
||||
children?: TChildren;
|
||||
title: string;
|
||||
showServiceGroupSaveButton?: boolean;
|
||||
}): Record<
|
||||
TPath,
|
||||
{
|
||||
element: React.ReactElement<any, any>;
|
||||
}
|
||||
} & (TChildren extends Record<string, Route> ? { children: TChildren } : {})
|
||||
> {
|
||||
return {
|
||||
[path]: {
|
||||
|
@ -52,8 +61,9 @@ function page<TPath extends string>({
|
|||
</ApmMainTemplate>
|
||||
</Breadcrumb>
|
||||
),
|
||||
children,
|
||||
},
|
||||
} as Record<TPath, { element: React.ReactElement<any, any> }>;
|
||||
} as any;
|
||||
}
|
||||
|
||||
function serviceGroupPage<TPath extends string>({
|
||||
|
@ -155,19 +165,58 @@ export const home = {
|
|||
element: <ServiceInventory />,
|
||||
serviceGroupContextTab: 'service-inventory',
|
||||
}),
|
||||
...page({
|
||||
path: '/traces',
|
||||
title: i18n.translate('xpack.apm.views.traceOverview.title', {
|
||||
defaultMessage: 'Traces',
|
||||
}),
|
||||
element: <TraceOverview />,
|
||||
}),
|
||||
...serviceGroupPage({
|
||||
path: '/service-map',
|
||||
title: ServiceMapTitle,
|
||||
element: <ServiceMapHome />,
|
||||
serviceGroupContextTab: 'service-map',
|
||||
}),
|
||||
...page({
|
||||
path: '/traces',
|
||||
title: i18n.translate('xpack.apm.views.traceOverview.title', {
|
||||
defaultMessage: 'Traces',
|
||||
}),
|
||||
element: (
|
||||
<TraceOverview>
|
||||
<Outlet />
|
||||
</TraceOverview>
|
||||
),
|
||||
children: {
|
||||
'/traces/explorer': {
|
||||
element: <TraceExplorer />,
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
query: t.string,
|
||||
type: t.union([
|
||||
t.literal(TraceSearchType.kql),
|
||||
t.literal(TraceSearchType.eql),
|
||||
]),
|
||||
waterfallItemId: t.string,
|
||||
traceId: t.string,
|
||||
transactionId: t.string,
|
||||
detailTab: t.union([
|
||||
t.literal(TransactionTab.timeline),
|
||||
t.literal(TransactionTab.metadata),
|
||||
t.literal(TransactionTab.logs),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
defaults: {
|
||||
query: {
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
waterfallItemId: '',
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
},
|
||||
},
|
||||
'/traces': {
|
||||
element: <TopTracesOverview />,
|
||||
},
|
||||
},
|
||||
}),
|
||||
'/backends': {
|
||||
element: <Outlet />,
|
||||
params: t.partial({
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 { useApmParams } from '../../../hooks/use_apm_params';
|
||||
|
||||
import { DatePicker } from '.';
|
||||
import { useTimeRangeId } from '../../../context/time_range_id/use_time_range_id';
|
||||
import {
|
||||
toBoolean,
|
||||
toNumber,
|
||||
} from '../../../context/url_params_context/helpers';
|
||||
|
||||
export function ApmDatePicker() {
|
||||
const { query } = useApmParams('/*');
|
||||
|
||||
if (!('rangeFrom' in query)) {
|
||||
throw new Error('range not available in route parameters');
|
||||
}
|
||||
|
||||
const {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused: refreshPausedFromUrl = 'true',
|
||||
refreshInterval: refreshIntervalFromUrl = '0',
|
||||
} = query;
|
||||
|
||||
const refreshPaused = toBoolean(refreshPausedFromUrl);
|
||||
|
||||
const refreshInterval = toNumber(refreshIntervalFromUrl);
|
||||
|
||||
const { incrementTimeRangeId } = useTimeRangeId();
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
refreshPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeRangeRefresh={() => {
|
||||
incrementTimeRangeId();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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 { Editor, IEditSession, TokenInfo as AceTokenInfo } from 'brace';
|
||||
import { Maybe } from '../../../../typings/common';
|
||||
import { EQLCodeEditorSuggestionType } from './constants';
|
||||
import { EQLToken } from './tokens';
|
||||
import {
|
||||
EQLCodeEditorSuggestion,
|
||||
EQLCodeEditorSuggestionCallback,
|
||||
EQLCodeEditorSuggestionRequest,
|
||||
} from './types';
|
||||
|
||||
type TokenInfo = AceTokenInfo & {
|
||||
type: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export class EQLCodeEditorCompleter {
|
||||
callback?: EQLCodeEditorSuggestionCallback;
|
||||
|
||||
private async getCompletionsAsync(
|
||||
session: IEditSession,
|
||||
position: { row: number; column: number },
|
||||
prefix: string | undefined
|
||||
): Promise<EQLCodeEditorSuggestion[]> {
|
||||
const token = session.getTokenAt(
|
||||
position.row,
|
||||
position.column
|
||||
) as Maybe<TokenInfo>;
|
||||
const tokensInLine = session.getTokens(position.row) as TokenInfo[];
|
||||
|
||||
function withWhitespace(
|
||||
vals: EQLCodeEditorSuggestion[],
|
||||
options: {
|
||||
before?: string;
|
||||
after?: string;
|
||||
} = {}
|
||||
) {
|
||||
const { after = ' ' } = options;
|
||||
let { before = ' ' } = options;
|
||||
|
||||
if (
|
||||
before &&
|
||||
(token?.value.match(/^\s+$/) || (token && token.type !== 'text'))
|
||||
) {
|
||||
before = before.trimLeft();
|
||||
}
|
||||
|
||||
return vals.map((val) => {
|
||||
const suggestion = typeof val === 'string' ? { value: val } : val;
|
||||
const valueAsString = suggestion.value;
|
||||
|
||||
return {
|
||||
...suggestion,
|
||||
caption: valueAsString,
|
||||
value: [before, valueAsString, after].join(''),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
position.row === 0 &&
|
||||
(!token || token.index === 0) &&
|
||||
'sequence by'.includes(prefix || '')
|
||||
) {
|
||||
return withWhitespace(['sequence by'], {
|
||||
before: '',
|
||||
after: ' ',
|
||||
});
|
||||
}
|
||||
|
||||
const previousTokens = tokensInLine
|
||||
.slice(0, token ? tokensInLine.indexOf(token) : tokensInLine.length)
|
||||
.reverse();
|
||||
|
||||
const completedEqlToken = previousTokens.find((t) =>
|
||||
t.type.startsWith('eql.')
|
||||
);
|
||||
|
||||
switch (completedEqlToken?.type) {
|
||||
case undefined:
|
||||
return [
|
||||
...withWhitespace(['['], { before: '', after: ' ' }),
|
||||
...(position.row > 2
|
||||
? withWhitespace(['until'], { before: '', after: ' [ ' })
|
||||
: []),
|
||||
];
|
||||
|
||||
case EQLToken.Sequence:
|
||||
return withWhitespace(
|
||||
await this.getExternalSuggestions({
|
||||
type: EQLCodeEditorSuggestionType.Field,
|
||||
}),
|
||||
{
|
||||
after: '\n\t[ ',
|
||||
}
|
||||
);
|
||||
|
||||
case EQLToken.SequenceItemStart:
|
||||
return withWhitespace(
|
||||
[
|
||||
...(await this.getExternalSuggestions({
|
||||
type: EQLCodeEditorSuggestionType.EventType,
|
||||
})),
|
||||
'any',
|
||||
],
|
||||
{ after: ' where ' }
|
||||
);
|
||||
|
||||
case EQLToken.EventType:
|
||||
return withWhitespace(['where']);
|
||||
|
||||
case EQLToken.Where:
|
||||
case EQLToken.LogicalOperator:
|
||||
return [
|
||||
...withWhitespace(
|
||||
await this.getExternalSuggestions({
|
||||
type: EQLCodeEditorSuggestionType.Field,
|
||||
})
|
||||
),
|
||||
...withWhitespace(['true', 'false'], { after: ' ]\n\t' }),
|
||||
];
|
||||
|
||||
case EQLToken.BoolCondition:
|
||||
return withWhitespace([']'], { after: '\n\t' });
|
||||
|
||||
case EQLToken.Operator:
|
||||
case EQLToken.InOperator:
|
||||
const field =
|
||||
previousTokens?.find((t) => t.type === EQLToken.Field)?.value ?? '';
|
||||
|
||||
const hasStartedValueLiteral =
|
||||
!!prefix?.trim() || token?.value.trim() === '"';
|
||||
|
||||
return withWhitespace(
|
||||
await this.getExternalSuggestions({
|
||||
type: EQLCodeEditorSuggestionType.Value,
|
||||
field,
|
||||
value: prefix ?? '',
|
||||
}),
|
||||
{ before: hasStartedValueLiteral ? '' : ' "', after: '" ' }
|
||||
);
|
||||
|
||||
case EQLToken.Value:
|
||||
return [
|
||||
...withWhitespace([']'], { after: '\n\t' }),
|
||||
...withWhitespace(['and', 'or']),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private async getExternalSuggestions(
|
||||
request: EQLCodeEditorSuggestionRequest
|
||||
): Promise<EQLCodeEditorSuggestion[]> {
|
||||
if (this.callback) {
|
||||
return this.callback(request);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getCompletions(
|
||||
_: Editor,
|
||||
session: IEditSession,
|
||||
position: { row: number; column: number },
|
||||
prefix: string | undefined,
|
||||
cb: (err: Error | null, suggestions?: EQLCodeEditorSuggestion[]) => void
|
||||
) {
|
||||
this.getCompletionsAsync(session, position, prefix)
|
||||
.then((suggestions) => {
|
||||
cb(
|
||||
null,
|
||||
suggestions.map((sugg) => {
|
||||
const suggestion =
|
||||
typeof sugg === 'string'
|
||||
? { value: sugg, score: 1000 }
|
||||
: { score: 1000, ...sugg };
|
||||
|
||||
return suggestion;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(cb);
|
||||
}
|
||||
|
||||
setSuggestionCb(cb?: EQLCodeEditorSuggestionCallback) {
|
||||
this.callback = cb;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const EQL_MODE_NAME = 'ace/mode/eql';
|
||||
export const EQL_THEME_NAME = 'ace/theme/eql';
|
||||
|
||||
export enum EQLCodeEditorSuggestionType {
|
||||
EventType = 'eventType',
|
||||
Field = 'field',
|
||||
Value = 'value',
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 'brace/ext/language_tools';
|
||||
import { acequire } from 'brace';
|
||||
import { EQLToken } from './tokens';
|
||||
|
||||
const TextHighlightRules = acequire(
|
||||
'ace/mode/text_highlight_rules'
|
||||
).TextHighlightRules;
|
||||
|
||||
export class EQLHighlightRules extends TextHighlightRules {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const fieldNameOrValueRegex = /((?:[^\s]+)|(?:".*?"))/;
|
||||
const operatorRegex = /(:|==|>|>=|<|<=|!=)/;
|
||||
|
||||
const sequenceItemEnd = {
|
||||
token: EQLToken.SequenceItemEnd,
|
||||
regex: /(\])/,
|
||||
next: 'start',
|
||||
};
|
||||
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: EQLToken.Sequence,
|
||||
regex: /(sequence by)/,
|
||||
next: 'field',
|
||||
},
|
||||
{
|
||||
token: EQLToken.SequenceItemStart,
|
||||
regex: /(\[)/,
|
||||
next: 'sequence_item',
|
||||
},
|
||||
{
|
||||
token: EQLToken.Until,
|
||||
regex: /(until)/,
|
||||
next: 'start',
|
||||
},
|
||||
],
|
||||
field: [
|
||||
{
|
||||
token: EQLToken.Field,
|
||||
regex: fieldNameOrValueRegex,
|
||||
next: 'start',
|
||||
},
|
||||
],
|
||||
sequence_item: [
|
||||
{
|
||||
token: EQLToken.EventType,
|
||||
regex: fieldNameOrValueRegex,
|
||||
next: 'where',
|
||||
},
|
||||
],
|
||||
sequence_item_end: [sequenceItemEnd],
|
||||
where: [
|
||||
{
|
||||
token: EQLToken.Where,
|
||||
regex: /(where)/,
|
||||
next: 'condition',
|
||||
},
|
||||
],
|
||||
condition: [
|
||||
{
|
||||
token: EQLToken.BoolCondition,
|
||||
regex: /(true|false)/,
|
||||
next: 'sequence_item_end',
|
||||
},
|
||||
{
|
||||
token: EQLToken.Field,
|
||||
regex: fieldNameOrValueRegex,
|
||||
next: 'comparison_operator',
|
||||
},
|
||||
],
|
||||
comparison_operator: [
|
||||
{
|
||||
token: EQLToken.Operator,
|
||||
regex: operatorRegex,
|
||||
next: 'value_or_value_list',
|
||||
},
|
||||
],
|
||||
value_or_value_list: [
|
||||
{
|
||||
token: EQLToken.Value,
|
||||
regex: /("([^"]+)")|([\d+\.]+)|(true|false|null)/,
|
||||
next: 'logical_operator_or_sequence_item_end',
|
||||
},
|
||||
{
|
||||
token: EQLToken.InOperator,
|
||||
regex: /(in)/,
|
||||
next: 'value_list',
|
||||
},
|
||||
],
|
||||
logical_operator_or_sequence_item_end: [
|
||||
{
|
||||
token: EQLToken.LogicalOperator,
|
||||
regex: /(and|or|not)/,
|
||||
next: 'condition',
|
||||
},
|
||||
sequenceItemEnd,
|
||||
],
|
||||
value_list: [
|
||||
{
|
||||
token: EQLToken.ValueListStart,
|
||||
regex: /(\()/,
|
||||
next: 'value_list_item',
|
||||
},
|
||||
],
|
||||
value_list_item: [
|
||||
{
|
||||
token: EQLToken.Value,
|
||||
regex: fieldNameOrValueRegex,
|
||||
next: 'comma',
|
||||
},
|
||||
],
|
||||
comma: [
|
||||
{
|
||||
token: EQLToken.Comma,
|
||||
regex: /,/,
|
||||
next: 'value_list_item_or_end',
|
||||
},
|
||||
],
|
||||
value_list_item_or_end: [
|
||||
{
|
||||
token: EQLToken.Value,
|
||||
regex: fieldNameOrValueRegex,
|
||||
next: 'comma',
|
||||
},
|
||||
{
|
||||
token: EQLToken.ValueListEnd,
|
||||
regex: /\)/,
|
||||
next: 'logical_operator_or_sequence_item_end',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
this.normalizeRules();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { TextMode as TextModeInterface, acequire } from 'brace';
|
||||
import { EQL_MODE_NAME } from './constants';
|
||||
import { EQLHighlightRules } from './eql_highlight_rules';
|
||||
|
||||
type ITextMode = new () => TextModeInterface;
|
||||
|
||||
const TextMode = acequire('ace/mode/text').Mode as ITextMode;
|
||||
|
||||
export class EQLMode extends TextMode {
|
||||
HighlightRules: typeof EQLHighlightRules;
|
||||
$id: string;
|
||||
constructor() {
|
||||
super();
|
||||
this.$id = EQL_MODE_NAME;
|
||||
this.HighlightRules = EQLHighlightRules;
|
||||
}
|
||||
}
|
|
@ -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 'brace/ext/language_tools';
|
||||
import { last } from 'lodash';
|
||||
import React, { useRef } from 'react';
|
||||
import { EuiCodeEditor } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { EQLCodeEditorCompleter } from './completer';
|
||||
import { EQL_THEME_NAME } from './constants';
|
||||
import { EQLMode } from './eql_mode';
|
||||
import './theme';
|
||||
import { EQLCodeEditorProps } from './types';
|
||||
|
||||
export function EQLCodeEditor(props: EQLCodeEditorProps) {
|
||||
const {
|
||||
showGutter = false,
|
||||
setOptions,
|
||||
getSuggestions,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const completer = useRef(new EQLCodeEditorCompleter());
|
||||
const eqlMode = useRef(new EQLMode());
|
||||
|
||||
completer.current.setSuggestionCb(getSuggestions);
|
||||
|
||||
const options = {
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
wrap: true,
|
||||
...setOptions,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="euiTextArea" style={{ maxWidth: 'none' }}>
|
||||
<EuiCodeEditor
|
||||
showGutter={showGutter}
|
||||
mode={eqlMode.current}
|
||||
theme={last(EQL_THEME_NAME.split('/'))}
|
||||
setOptions={options}
|
||||
onAceEditorRef={(editor) => {
|
||||
if (editor) {
|
||||
editor.editor.completers = [completer.current];
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { once } from 'lodash';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EQLCodeEditorProps } from './types';
|
||||
|
||||
const loadEqlCodeEditor = once(() => import('.').then((m) => m.EQLCodeEditor));
|
||||
|
||||
type EQLCodeEditorComponentType = Awaited<ReturnType<typeof loadEqlCodeEditor>>;
|
||||
|
||||
export function LazilyLoadedEQLCodeEditor(props: EQLCodeEditorProps) {
|
||||
const [EQLCodeEditor, setEQLCodeEditor] = useState<
|
||||
EQLCodeEditorComponentType | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
loadEqlCodeEditor().then((editor) => {
|
||||
setEQLCodeEditor(() => {
|
||||
return editor;
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
return EQLCodeEditor ? (
|
||||
<EQLCodeEditor {...props} />
|
||||
) : (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem>
|
||||
<EuiLoadingSpinner size="s" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { euiLightVars as theme } from '@kbn/ui-theme';
|
||||
import { EQL_THEME_NAME } from './constants';
|
||||
|
||||
// @ts-expect-error
|
||||
ace.define(
|
||||
EQL_THEME_NAME,
|
||||
['require', 'exports', 'module', 'ace/lib/dom'],
|
||||
function (acequire: any, exports: any) {
|
||||
exports.isDark = false;
|
||||
exports.cssClass = 'ace-eql';
|
||||
exports.cssText = `
|
||||
.ace-eql .ace_scroller {
|
||||
background-color: transparent;
|
||||
}
|
||||
.ace-eql .ace_marker-layer .ace_selection {
|
||||
background: rgb(181, 213, 255);
|
||||
}
|
||||
.ace-eql .ace_placeholder {
|
||||
color: ${theme.euiTextSubduedColor};
|
||||
padding: 0;
|
||||
}
|
||||
.ace-eql .ace_sequence,
|
||||
.ace-eql .ace_where,
|
||||
.ace-eql .ace_until {
|
||||
color: ${theme.euiColorDarkShade};
|
||||
}
|
||||
.ace-eql .ace_sequence_item_start,
|
||||
.ace-eql .ace_sequence_item_end,
|
||||
.ace-eql .ace_operator,
|
||||
.ace-eql .ace_logical_operator {
|
||||
color: ${theme.euiColorMediumShade};
|
||||
}
|
||||
.ace-eql .ace_value,
|
||||
.ace-eql .ace_bool_condition {
|
||||
color: ${theme.euiColorAccent};
|
||||
}
|
||||
.ace-eql .ace_event_type,
|
||||
.ace-eql .ace_field {
|
||||
color: ${theme.euiColorPrimaryText};
|
||||
}
|
||||
// .ace-eql .ace_gutter {
|
||||
// color: #333;
|
||||
// }
|
||||
.ace-eql .ace_print-margin {
|
||||
width: 1px;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
.ace-eql .ace_fold {
|
||||
background-color: #6B72E6;
|
||||
}
|
||||
.ace-eql .ace_cursor {
|
||||
color: black;
|
||||
}
|
||||
.ace-eql .ace_invisible {
|
||||
color: rgb(191, 191, 191);
|
||||
}
|
||||
.ace-eql .ace_marker-layer .ace_selection {
|
||||
background: rgb(181, 213, 255);
|
||||
}
|
||||
.ace-eql.ace_multiselect .ace_selection.ace_start {
|
||||
box-shadow: 0 0 3px 0px white;
|
||||
}
|
||||
.ace-eql .ace_marker-layer .ace_step {
|
||||
background: rgb(252, 255, 0);
|
||||
}
|
||||
.ace-eql .ace_marker-layer .ace_stack {
|
||||
background: rgb(164, 229, 101);
|
||||
}
|
||||
.ace-eql .ace_marker-layer .ace_bracket {
|
||||
margin: -1px 0 0 -1px;
|
||||
border: 1px solid rgb(192, 192, 192);
|
||||
}
|
||||
.ace-eql .ace_marker-layer .ace_selected-word {
|
||||
background: rgb(250, 250, 255);
|
||||
border: 1px solid rgb(200, 200, 250);
|
||||
}
|
||||
.ace-eql .ace_indent-guide {
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;
|
||||
}`;
|
||||
|
||||
const dom = acequire('../lib/dom');
|
||||
dom.importCssString(exports.cssText, exports.cssClass);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* 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 enum EQLToken {
|
||||
Sequence = 'eql.sequence',
|
||||
SequenceItemStart = 'eql.sequence_item_start',
|
||||
SequenceItemEnd = 'eql.sequence_item_end',
|
||||
Until = 'eql.until',
|
||||
Field = 'eql.field',
|
||||
EventType = 'eql.event_type',
|
||||
Where = 'eql.where',
|
||||
BoolCondition = 'eql.bool_condition',
|
||||
Operator = 'eql.operator',
|
||||
Value = 'eql.value',
|
||||
LogicalOperator = 'eql.logical_operator',
|
||||
InOperator = 'eql.in_operator',
|
||||
ValueListStart = 'eql.value_list_start',
|
||||
ValueListItem = 'eql.value_list_item',
|
||||
ValueListEnd = 'eql.value_list_end',
|
||||
Comma = 'eql.comma',
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiCodeEditorProps } from '@kbn/es-ui-shared-plugin/public';
|
||||
import { EQLCodeEditorSuggestionType } from './constants';
|
||||
|
||||
export type EQLCodeEditorSuggestion =
|
||||
| string
|
||||
| { value: string; score?: number };
|
||||
|
||||
export type EQLCodeEditorSuggestionRequest =
|
||||
| {
|
||||
type:
|
||||
| EQLCodeEditorSuggestionType.EventType
|
||||
| EQLCodeEditorSuggestionType.Field;
|
||||
}
|
||||
| { type: EQLCodeEditorSuggestionType.Value; field: string; value: string };
|
||||
|
||||
export type EQLCodeEditorSuggestionCallback = (
|
||||
request: EQLCodeEditorSuggestionRequest
|
||||
) => Promise<EQLCodeEditorSuggestion[]>;
|
||||
|
||||
export type EQLCodeEditorProps = Omit<
|
||||
EuiCodeEditorProps,
|
||||
'mode' | 'theme' | 'setOptions'
|
||||
> & {
|
||||
getSuggestions?: EQLCodeEditorSuggestionCallback;
|
||||
setOptions?: EuiCodeEditorProps['setOptions'];
|
||||
};
|
|
@ -12,7 +12,7 @@ import React from 'react';
|
|||
import { useLocation } from 'react-router-dom';
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import url from 'url';
|
||||
import { APM_STATIC_INDEX_PATTERN_ID } from '../../../../../common/index_pattern_constants';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../../../common/data_view_constants';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { getTimepickerRisonData } from '../rison_helpers';
|
||||
|
||||
|
@ -46,7 +46,7 @@ export const getDiscoverHref = ({
|
|||
_g: getTimepickerRisonData(location.search),
|
||||
_a: {
|
||||
...query._a,
|
||||
index: APM_STATIC_INDEX_PATTERN_ID,
|
||||
index: APM_STATIC_DATA_VIEW_ID,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -13,11 +13,8 @@ import {
|
|||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useTimeRangeId } from '../../context/time_range_id/use_time_range_id';
|
||||
import { toBoolean, toNumber } from '../../context/url_params_context/helpers';
|
||||
import { useApmParams } from '../../hooks/use_apm_params';
|
||||
import { useBreakpoints } from '../../hooks/use_breakpoints';
|
||||
import { DatePicker } from './date_picker';
|
||||
import { ApmDatePicker } from './date_picker/apm_date_picker';
|
||||
import { KueryBar } from './kuery_bar';
|
||||
import { TimeComparison } from './time_comparison';
|
||||
import { TransactionTypeSelect } from './transaction_type_select';
|
||||
|
@ -31,39 +28,6 @@ interface Props {
|
|||
kueryBarBoolFilter?: QueryDslQueryContainer[];
|
||||
}
|
||||
|
||||
function ApmDatePicker() {
|
||||
const { query } = useApmParams('/*');
|
||||
|
||||
if (!('rangeFrom' in query)) {
|
||||
throw new Error('range not available in route parameters');
|
||||
}
|
||||
|
||||
const {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused: refreshPausedFromUrl = 'true',
|
||||
refreshInterval: refreshIntervalFromUrl = '0',
|
||||
} = query;
|
||||
|
||||
const refreshPaused = toBoolean(refreshPausedFromUrl);
|
||||
|
||||
const refreshInterval = toNumber(refreshIntervalFromUrl);
|
||||
|
||||
const { incrementTimeRangeId } = useTimeRangeId();
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
rangeFrom={rangeFrom}
|
||||
rangeTo={rangeTo}
|
||||
refreshPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeRangeRefresh={() => {
|
||||
incrementTimeRangeId();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SearchBar({
|
||||
hidden = false,
|
||||
showKueryBar = true,
|
||||
|
|
|
@ -11,6 +11,8 @@ import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/pu
|
|||
import { MapsStartApi } from '@kbn/maps-plugin/public';
|
||||
import { ObservabilityPublicStart } from '@kbn/observability-plugin/public';
|
||||
import { Start as InspectorPluginStart } from '@kbn/inspector-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { ApmPluginSetupDeps } from '../../plugin';
|
||||
import { ConfigSchema } from '../..';
|
||||
|
||||
|
@ -22,6 +24,8 @@ export interface ApmPluginContextValue {
|
|||
plugins: ApmPluginSetupDeps & { maps?: MapsStartApi };
|
||||
observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry;
|
||||
observability: ObservabilityPublicStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
}
|
||||
|
||||
export const ApmPluginContext = createContext({} as ApmPluginContextValue);
|
||||
|
|
14
x-pack/plugins/apm/public/hooks/use_apm_route_path.ts
Normal file
14
x-pack/plugins/apm/public/hooks/use_apm_route_path.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useRoutePath, PathsOf } from '@kbn/typed-react-router-config';
|
||||
import { ApmRoutes } from '../components/routing/apm_route_config';
|
||||
|
||||
export function useApmRoutePath() {
|
||||
const path = useRoutePath();
|
||||
|
||||
return path as PathsOf<ApmRoutes>;
|
||||
}
|
16
x-pack/plugins/apm/public/hooks/use_static_data_view.ts
Normal file
16
x-pack/plugins/apm/public/hooks/use_static_data_view.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 useAsync from 'react-use/lib/useAsync';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants';
|
||||
|
||||
export function useStaticDataView() {
|
||||
const { dataViews } = useApmPluginContext();
|
||||
|
||||
return useAsync(() => dataViews.get(APM_STATIC_DATA_VIEW_ID));
|
||||
}
|
|
@ -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 { apmTraceExplorerTab } from '@kbn/observability-plugin/common';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
export function useTraceExplorerEnabledSetting() {
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
return core.uiSettings.get<boolean>(apmTraceExplorerTab, false);
|
||||
}
|
|
@ -47,6 +47,7 @@ import type {
|
|||
import type { SecurityPluginStart } from '@kbn/security-plugin/public';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { enableServiceGroups } from '@kbn/observability-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { registerApmAlerts } from './components/alerting/register_apm_alerts';
|
||||
import {
|
||||
getApmEnrollmentFlyoutData,
|
||||
|
@ -88,6 +89,8 @@ export interface ApmPluginStartDeps {
|
|||
fleet?: FleetStart;
|
||||
security?: SecurityPluginStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
}
|
||||
|
||||
const servicesTitle = i18n.translate('xpack.apm.navigation.servicesTitle', {
|
||||
|
|
|
@ -36,7 +36,7 @@ export async function callAsyncWithDebug<T>({
|
|||
requestParams: Record<string, any>;
|
||||
operationName: string;
|
||||
isCalledWithInternalUser: boolean; // only allow inspection of queries that were retrieved with credentials of the end user
|
||||
}) {
|
||||
}): Promise<T> {
|
||||
if (!debug) {
|
||||
return cb();
|
||||
}
|
||||
|
|
|
@ -11,10 +11,10 @@ export function cancelEsRequestOnAbort<T extends Promise<any>>(
|
|||
promise: T,
|
||||
request: KibanaRequest,
|
||||
controller: AbortController
|
||||
) {
|
||||
): T {
|
||||
const subscription = request.events.aborted$.subscribe(() => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
return promise.finally(() => subscription.unsubscribe());
|
||||
return promise.finally(() => subscription.unsubscribe()) as T;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type {
|
||||
EqlSearchRequest,
|
||||
TermsEnumRequest,
|
||||
TermsEnumResponse,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
@ -30,7 +31,10 @@ import {
|
|||
getDebugTitle,
|
||||
} from '../call_async_with_debug';
|
||||
import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort';
|
||||
import { unpackProcessorEvents } from './unpack_processor_events';
|
||||
import {
|
||||
unpackProcessorEvents,
|
||||
processorEventsToIndex,
|
||||
} from './unpack_processor_events';
|
||||
|
||||
export type APMEventESSearchRequest = Omit<ESSearchRequest, 'index'> & {
|
||||
apm: {
|
||||
|
@ -46,6 +50,10 @@ export type APMEventESTermsEnumRequest = Omit<TermsEnumRequest, 'index'> & {
|
|||
apm: { events: ProcessorEvent[] };
|
||||
};
|
||||
|
||||
export type APMEventEqlSearchRequest = Omit<EqlSearchRequest, 'index'> & {
|
||||
apm: { events: ProcessorEvent[] };
|
||||
};
|
||||
|
||||
// These keys shoul all be `ProcessorEvent.x`, but until TypeScript 4.2 we're inlining them here.
|
||||
// See https://github.com/microsoft/TypeScript/issues/37888
|
||||
type TypeOfProcessorEvent<T extends ProcessorEvent> = {
|
||||
|
@ -114,7 +122,7 @@ export class APMEventClient {
|
|||
this.esClient.search(searchParams, {
|
||||
signal: controller.signal,
|
||||
meta: true,
|
||||
}),
|
||||
}) as Promise<any>,
|
||||
this.request,
|
||||
controller
|
||||
);
|
||||
|
@ -139,6 +147,48 @@ export class APMEventClient {
|
|||
});
|
||||
}
|
||||
|
||||
async eqlSearch(operationName: string, params: APMEventEqlSearchRequest) {
|
||||
const requestType = 'eql_search';
|
||||
const index = processorEventsToIndex(params.apm.events, this.indices);
|
||||
|
||||
return callAsyncWithDebug({
|
||||
cb: () => {
|
||||
const { apm, ...rest } = params;
|
||||
|
||||
const eqlSearchPromise = withApmSpan(operationName, () => {
|
||||
const controller = new AbortController();
|
||||
return cancelEsRequestOnAbort(
|
||||
this.esClient.eql.search(
|
||||
{
|
||||
index,
|
||||
...rest,
|
||||
},
|
||||
{ signal: controller.signal, meta: true }
|
||||
),
|
||||
this.request,
|
||||
controller
|
||||
);
|
||||
});
|
||||
|
||||
return unwrapEsResponse(eqlSearchPromise);
|
||||
},
|
||||
getDebugMessage: () => ({
|
||||
body: getDebugBody({
|
||||
params,
|
||||
requestType,
|
||||
operationName,
|
||||
}),
|
||||
title: getDebugTitle(this.request),
|
||||
}),
|
||||
isCalledWithInternalUser: false,
|
||||
debug: this.debug,
|
||||
request: this.request,
|
||||
requestType,
|
||||
operationName,
|
||||
requestParams: params,
|
||||
});
|
||||
}
|
||||
|
||||
async termsEnum(
|
||||
operationName: string,
|
||||
params: APMEventESTermsEnumRequest
|
||||
|
|
|
@ -9,7 +9,6 @@ import { uniq, defaultsDeep, cloneDeep } from 'lodash';
|
|||
import { ESSearchRequest, ESFilter } from '@kbn/core/types/elasticsearch';
|
||||
import { PROCESSOR_EVENT } from '../../../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../../../common/processor_event';
|
||||
import { APMEventESSearchRequest, APMEventESTermsEnumRequest } from '.';
|
||||
import { ApmIndicesConfig } from '../../../../routes/settings/apm_indices/get_apm_indices';
|
||||
|
||||
const processorEventIndexMap = {
|
||||
|
@ -21,13 +20,24 @@ const processorEventIndexMap = {
|
|||
[ProcessorEvent.profile]: 'transaction',
|
||||
} as const;
|
||||
|
||||
export function processorEventsToIndex(
|
||||
events: ProcessorEvent[],
|
||||
indices: ApmIndicesConfig
|
||||
) {
|
||||
return uniq(events.map((event) => indices[processorEventIndexMap[event]]));
|
||||
}
|
||||
|
||||
export function unpackProcessorEvents(
|
||||
request: APMEventESSearchRequest | APMEventESTermsEnumRequest,
|
||||
request: {
|
||||
apm: {
|
||||
events: ProcessorEvent[];
|
||||
};
|
||||
},
|
||||
indices: ApmIndicesConfig
|
||||
) {
|
||||
const { apm, ...params } = request;
|
||||
const events = uniq(apm.events);
|
||||
const index = events.map((event) => indices[processorEventIndexMap[event]]);
|
||||
const index = processorEventsToIndex(events, indices);
|
||||
|
||||
const withFilterForProcessorEvent: ESSearchRequest & {
|
||||
body: { query: { bool: { filter: ESFilter[] } } };
|
||||
|
|
|
@ -74,7 +74,11 @@ export async function createInternalESClient({
|
|||
): Promise<ESSearchResponse<TDocument, TSearchRequest>> => {
|
||||
return callEs(operationName, {
|
||||
requestType: 'search',
|
||||
cb: (signal) => asInternalUser.search(params, { signal, meta: true }),
|
||||
cb: (signal) =>
|
||||
asInternalUser.search(params, {
|
||||
signal,
|
||||
meta: true,
|
||||
}) as Promise<{ body: any }>,
|
||||
params,
|
||||
});
|
||||
},
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { APM_STATIC_INDEX_PATTERN_ID } from '../../../common/index_pattern_constants';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../../common/data_view_constants';
|
||||
import { hasHistoricalAgentData } from '../historical_data/has_historical_agent_data';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { APMRouteHandlerResources } from '../typings';
|
||||
|
@ -55,7 +55,7 @@ export async function createStaticDataView({
|
|||
'index-pattern',
|
||||
getApmDataViewAttributes(apmDataViewTitle),
|
||||
{
|
||||
id: APM_STATIC_INDEX_PATTERN_ID,
|
||||
id: APM_STATIC_DATA_VIEW_ID,
|
||||
overwrite: forceOverwrite,
|
||||
namespace: spaceId,
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ async function getForceOverwrite({
|
|||
const existingDataView =
|
||||
await savedObjectsClient.get<ApmDataViewAttributes>(
|
||||
'index-pattern',
|
||||
APM_STATIC_INDEX_PATTERN_ID
|
||||
APM_STATIC_DATA_VIEW_ID
|
||||
);
|
||||
|
||||
// if the existing data view does not matches the new one, force an update
|
||||
|
|
|
@ -115,7 +115,7 @@ export function transformServiceMapResponses(response: ServiceMapResponse) {
|
|||
? anomalies.serviceAnomalies.find(
|
||||
(item) => item.serviceName === serviceName
|
||||
)
|
||||
: null;
|
||||
: undefined;
|
||||
|
||||
if (matchedServiceNodes.length) {
|
||||
return {
|
||||
|
@ -158,9 +158,16 @@ export function transformServiceMapResponses(response: ServiceMapResponse) {
|
|||
const sourceData = getConnectionNode(connection.source);
|
||||
const targetData = getConnectionNode(connection.destination);
|
||||
|
||||
const label =
|
||||
sourceData[SERVICE_NAME] +
|
||||
' to ' +
|
||||
(targetData[SERVICE_NAME] ||
|
||||
targetData[SPAN_DESTINATION_SERVICE_RESOURCE]);
|
||||
|
||||
return {
|
||||
source: sourceData.id,
|
||||
target: targetData.id,
|
||||
label,
|
||||
id: getConnectionId({ source: sourceData, destination: targetData }),
|
||||
sourceData,
|
||||
targetData,
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 {
|
||||
rangeQuery,
|
||||
kqlQuery,
|
||||
termsQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { Environment } from '../../../common/environment_rt';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { TraceSearchType } from '../../../common/trace_explorer';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import {
|
||||
PARENT_ID,
|
||||
PROCESSOR_EVENT,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
TRANSACTION_SAMPLED,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { asMutableArray } from '../../../common/utils/as_mutable_array';
|
||||
|
||||
export async function getTraceSamplesByQuery({
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
query,
|
||||
type,
|
||||
}: {
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
environment: Environment;
|
||||
query: string;
|
||||
type: TraceSearchType;
|
||||
}) {
|
||||
const size = 500;
|
||||
|
||||
let traceIds: string[] = [];
|
||||
|
||||
if (type === TraceSearchType.kql) {
|
||||
traceIds =
|
||||
(
|
||||
await setup.apmEventClient.search('get_trace_ids_by_kql_query', {
|
||||
apm: {
|
||||
events: [
|
||||
ProcessorEvent.transaction,
|
||||
ProcessorEvent.span,
|
||||
ProcessorEvent.error,
|
||||
],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(query),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
traceId: {
|
||||
terms: {
|
||||
field: TRACE_ID,
|
||||
execution_hint: 'map',
|
||||
size,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).aggregations?.traceId.buckets.map((bucket) => bucket.key as string) ??
|
||||
[];
|
||||
} else if (type === TraceSearchType.eql) {
|
||||
traceIds =
|
||||
(
|
||||
await setup.apmEventClient.eqlSearch('get_trace_ids_by_eql_query', {
|
||||
apm: {
|
||||
events: [
|
||||
ProcessorEvent.transaction,
|
||||
ProcessorEvent.span,
|
||||
ProcessorEvent.error,
|
||||
],
|
||||
},
|
||||
body: {
|
||||
size: 1000,
|
||||
filter: {
|
||||
bool: {
|
||||
filter: [
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
],
|
||||
},
|
||||
},
|
||||
event_category_field: PROCESSOR_EVENT,
|
||||
query,
|
||||
},
|
||||
filter_path: 'hits.sequences.events._source.trace.id',
|
||||
})
|
||||
).hits?.sequences?.flatMap((sequence) =>
|
||||
sequence.events.map(
|
||||
(event) => (event._source as { trace: { id: string } }).trace.id
|
||||
)
|
||||
) ?? [];
|
||||
}
|
||||
|
||||
if (!traceIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const traceSamplesResponse = await setup.apmEventClient.search(
|
||||
'get_trace_samples_by_trace_ids',
|
||||
{
|
||||
apm: {
|
||||
events: [ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
[TRANSACTION_SAMPLED]: true,
|
||||
},
|
||||
},
|
||||
...termsQuery(TRACE_ID, ...traceIds),
|
||||
...rangeQuery(start, end),
|
||||
],
|
||||
must_not: [{ exists: { field: PARENT_ID } }],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
transactionId: {
|
||||
terms: {
|
||||
field: TRANSACTION_ID,
|
||||
size,
|
||||
},
|
||||
aggs: {
|
||||
latest: {
|
||||
top_metrics: {
|
||||
metrics: asMutableArray([{ field: TRACE_ID }] as const),
|
||||
size: 1,
|
||||
sort: {
|
||||
'@timestamp': 'desc' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
traceSamplesResponse.aggregations?.transactionId.buckets.map((bucket) => ({
|
||||
traceId: bucket.latest.top[0].metrics['trace.id'] as string,
|
||||
transactionId: bucket.key as string,
|
||||
})) ?? []
|
||||
);
|
||||
}
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { TraceSearchType } from '../../../common/trace_explorer';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { getTraceItems } from './get_trace_items';
|
||||
import { getTopTracesPrimaryStats } from './get_top_traces_primary_stats';
|
||||
import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import {
|
||||
environmentRt,
|
||||
|
@ -16,9 +16,11 @@ import {
|
|||
probabilityRt,
|
||||
rangeRt,
|
||||
} from '../default_api_types';
|
||||
import { getSearchAggregatedTransactions } from '../../lib/helpers/transactions';
|
||||
import { getRootTransactionByTraceId } from '../transactions/get_transaction_by_trace';
|
||||
import { getTransaction } from '../transactions/get_transaction';
|
||||
import { getRootTransactionByTraceId } from '../transactions/get_transaction_by_trace';
|
||||
import { getTopTracesPrimaryStats } from './get_top_traces_primary_stats';
|
||||
import { getTraceItems } from './get_trace_items';
|
||||
import { getTraceSamplesByQuery } from './get_trace_samples_by_query';
|
||||
|
||||
const tracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces',
|
||||
|
@ -135,9 +137,50 @@ const transactionByIdRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const findTracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces/find',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
rangeRt,
|
||||
environmentRt,
|
||||
t.type({
|
||||
query: t.string,
|
||||
type: t.union([
|
||||
t.literal(TraceSearchType.kql),
|
||||
t.literal(TraceSearchType.eql),
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
samples: Array<{ traceId: string; transactionId: string }>;
|
||||
}> => {
|
||||
const { start, end, environment, query, type } = resources.params.query;
|
||||
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
return {
|
||||
samples: await getTraceSamplesByQuery({
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
query,
|
||||
type,
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const traceRouteRepository = {
|
||||
...tracesByIdRoute,
|
||||
...tracesRoute,
|
||||
...rootTransactionByTraceIdRoute,
|
||||
...transactionByIdRoute,
|
||||
...findTracesRoute,
|
||||
};
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '@kbn/home-plugin/server';
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import { APMConfig } from '..';
|
||||
import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constants';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '../../common/data_view_constants';
|
||||
import { getApmDataViewAttributes } from '../routes/data_view/get_apm_data_view_attributes';
|
||||
import { getApmDataViewTitle } from '../routes/data_view/get_apm_data_view_title';
|
||||
import { ApmIndicesConfig } from '../routes/settings/apm_indices/get_apm_indices';
|
||||
|
@ -42,7 +42,7 @@ export const tutorialProvider =
|
|||
const dataViewTitle = getApmDataViewTitle(apmIndices);
|
||||
const savedObjects = [
|
||||
{
|
||||
id: APM_STATIC_INDEX_PATTERN_ID,
|
||||
id: APM_STATIC_DATA_VIEW_ID,
|
||||
attributes: getApmDataViewAttributes(dataViewTitle),
|
||||
type: 'index-pattern',
|
||||
},
|
||||
|
|
|
@ -17,6 +17,7 @@ export {
|
|||
defaultApmServiceEnvironment,
|
||||
apmServiceInventoryOptimizedSorting,
|
||||
apmProgressiveLoading,
|
||||
apmTraceExplorerTab,
|
||||
} from './ui_settings_keys';
|
||||
|
||||
export {
|
||||
|
|
|
@ -15,3 +15,4 @@ export const apmProgressiveLoading = 'observability:apmProgressiveLoading';
|
|||
export const enableServiceGroups = 'observability:enableServiceGroups';
|
||||
export const apmServiceInventoryOptimizedSorting =
|
||||
'observability:apmServiceInventoryOptimizedSorting';
|
||||
export const apmTraceExplorerTab = 'observability:apmTraceExplorerTab';
|
||||
|
|
|
@ -71,7 +71,7 @@ function getStats({
|
|||
},
|
||||
};
|
||||
|
||||
if (esResponse?.hits) {
|
||||
if (esResponse?.hits?.hits) {
|
||||
stats.hits = {
|
||||
label: i18n.translate('xpack.observability.inspector.stats.hitsLabel', {
|
||||
defaultMessage: 'Hits',
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
enableServiceGroups,
|
||||
apmServiceInventoryOptimizedSorting,
|
||||
enableNewSyntheticsView,
|
||||
apmTraceExplorerTab,
|
||||
} from '../common/ui_settings_keys';
|
||||
|
||||
const technicalPreviewLabel = i18n.translate(
|
||||
|
@ -187,4 +188,19 @@ export const uiSettings: Record<string, UiSettingsParams<boolean | number | stri
|
|||
requiresPageReload: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
[apmTraceExplorerTab]: {
|
||||
category: [observabilityFeatureId],
|
||||
name: i18n.translate('xpack.observability.apmTraceExplorerTab', {
|
||||
defaultMessage: 'APM Trace Explorer',
|
||||
}),
|
||||
description: i18n.translate('xpack.observability.apmTraceExplorerTabDescription', {
|
||||
defaultMessage:
|
||||
'{technicalPreviewLabel} Enable the APM Trace Explorer feature, that allows you to search and inspect traces with KQL or EQL',
|
||||
values: { technicalPreviewLabel: `<em>[${technicalPreviewLabel}]</em>` },
|
||||
}),
|
||||
schema: schema.boolean(),
|
||||
value: false,
|
||||
requiresPageReload: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { apm, ApmSynthtraceEsClient, timerange } from '@elastic/apm-synthtrace';
|
||||
import expect from '@kbn/expect';
|
||||
import { APM_STATIC_INDEX_PATTERN_ID } from '@kbn/apm-plugin/common/index_pattern_constants';
|
||||
import { APM_STATIC_DATA_VIEW_ID } from '@kbn/apm-plugin/common/data_view_constants';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { SupertestReturnType } from '../../common/apm_api_supertest';
|
||||
|
||||
|
@ -26,13 +26,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
function deleteDataView() {
|
||||
// return supertest.delete('/api/saved_objects/<type>/<id>').set('kbn-xsrf', 'foo').expect(200)
|
||||
return supertest
|
||||
.delete(`/api/saved_objects/index-pattern/${APM_STATIC_INDEX_PATTERN_ID}`)
|
||||
.delete(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(200);
|
||||
}
|
||||
|
||||
function getDataView() {
|
||||
return supertest.get(`/api/saved_objects/index-pattern/${APM_STATIC_INDEX_PATTERN_ID}`);
|
||||
return supertest.get(`/api/saved_objects/index-pattern/${APM_STATIC_DATA_VIEW_ID}`);
|
||||
}
|
||||
|
||||
function getDataViewSuggestions(field: string) {
|
||||
|
|
307
x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts
Normal file
307
x-pack/test/apm_api_integration/tests/traces/find_traces.spec.ts
Normal file
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 { apm, timerange } from '@elastic/apm-synthtrace';
|
||||
import expect from '@kbn/expect';
|
||||
import { TraceSearchType } from '@kbn/apm-plugin/common/trace_explorer';
|
||||
import { Environment } from '@kbn/apm-plugin/common/environment_rt';
|
||||
import { ENVIRONMENT_ALL } from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { sortBy } from 'lodash';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { ApmApiError } from '../../common/apm_api_supertest';
|
||||
|
||||
type Instance = ReturnType<ReturnType<typeof apm.service>['instance']>;
|
||||
type Transaction = ReturnType<Instance['transaction']>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
// for EQL sequences to work, events need a slight time offset,
|
||||
// as ES will sort based on @timestamp. to acommodate this offset
|
||||
// we also add a little bit of a buffer to the requested time range
|
||||
const endWithOffset = end + 100000;
|
||||
|
||||
async function fetchTraceSamples({
|
||||
query,
|
||||
type,
|
||||
environment,
|
||||
}: {
|
||||
query: string;
|
||||
type: TraceSearchType;
|
||||
environment: Environment;
|
||||
}) {
|
||||
return apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/find`,
|
||||
params: {
|
||||
query: {
|
||||
query,
|
||||
type,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(endWithOffset).toISOString(),
|
||||
environment,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function fetchTraces(samples: Array<{ traceId: string; transactionId: string }>) {
|
||||
if (!samples.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
samples.map(async ({ traceId }) => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(endWithOffset).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
return response.body.traceDocs;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Find traces when traces do not exist',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await fetchTraceSamples({
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({
|
||||
samples: [],
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Find traces when traces exist',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
before(() => {
|
||||
const java = apm.service('java', 'production', 'java').instance('java');
|
||||
|
||||
const node = apm.service('node', 'development', 'nodejs').instance('node');
|
||||
|
||||
const python = apm.service('python', 'production', 'python').instance('python');
|
||||
|
||||
function generateTrace(
|
||||
timestamp: number,
|
||||
order: Instance[],
|
||||
db?: 'elasticsearch' | 'redis'
|
||||
) {
|
||||
return order
|
||||
.concat()
|
||||
.reverse()
|
||||
.reduce<Transaction | undefined>((prev, instance, index) => {
|
||||
const invertedIndex = order.length - index - 1;
|
||||
|
||||
const duration = 50;
|
||||
const time = timestamp + invertedIndex * 10;
|
||||
|
||||
const transaction: Transaction = instance
|
||||
.transaction(`GET /${instance.fields['service.name']!}/api`)
|
||||
.timestamp(time)
|
||||
.duration(duration);
|
||||
|
||||
if (prev) {
|
||||
const next = order[invertedIndex + 1].fields['service.name']!;
|
||||
transaction.children(
|
||||
instance
|
||||
.span(`GET ${next}/api`, 'external', 'http')
|
||||
.destination(next)
|
||||
.duration(duration)
|
||||
.timestamp(time + 1)
|
||||
.children(prev)
|
||||
);
|
||||
} else if (db) {
|
||||
transaction.children(
|
||||
instance
|
||||
.span(db, 'db', db)
|
||||
.destination(db)
|
||||
.duration(duration)
|
||||
.timestamp(time + 1)
|
||||
);
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}, undefined)!;
|
||||
}
|
||||
|
||||
return synthtraceEsClient.index(
|
||||
timerange(start, end)
|
||||
.interval('15m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return [
|
||||
generateTrace(timestamp, [java, node]),
|
||||
generateTrace(timestamp, [node, java], 'redis'),
|
||||
generateTrace(timestamp, [python], 'redis'),
|
||||
generateTrace(timestamp, [python, node, java], 'elasticsearch'),
|
||||
generateTrace(timestamp, [java, python, node]),
|
||||
];
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('when using KQL', () => {
|
||||
describe('and the query is empty', () => {
|
||||
it('returns all trace samples', async () => {
|
||||
const {
|
||||
body: { samples },
|
||||
} = await fetchTraceSamples({
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
});
|
||||
|
||||
expect(samples.length).to.eql(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('and query is set', () => {
|
||||
it('returns the relevant traces', async () => {
|
||||
const {
|
||||
body: { samples },
|
||||
} = await fetchTraceSamples({
|
||||
query: 'span.destination.service.resource:elasticsearch',
|
||||
type: TraceSearchType.kql,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
});
|
||||
|
||||
expect(samples.length).to.eql(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using EQL', () => {
|
||||
describe('and the query is invalid', () => {
|
||||
it.skip('returns a 400', async function () {
|
||||
try {
|
||||
await fetchTraceSamples({
|
||||
query: '',
|
||||
type: TraceSearchType.eql,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
});
|
||||
this.fail();
|
||||
} catch (error: unknown) {
|
||||
const apiError = error as ApmApiError;
|
||||
expect(apiError.res.status).to.eql(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('and the query is set', () => {
|
||||
it('returns the correct trace samples for transaction sequences', async () => {
|
||||
const {
|
||||
body: { samples },
|
||||
} = await fetchTraceSamples({
|
||||
query: `sequence by trace.id
|
||||
[ transaction where service.name == "java" ]
|
||||
[ transaction where service.name == "node" ]`,
|
||||
type: TraceSearchType.eql,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
});
|
||||
|
||||
const traces = await fetchTraces(samples);
|
||||
|
||||
expect(traces.length).to.eql(2);
|
||||
|
||||
const mapped = traces.map((traceDocs) => {
|
||||
return sortBy(traceDocs, '@timestamp')
|
||||
.filter((doc) => doc.processor.event === 'transaction')
|
||||
.map((doc) => doc.service.name);
|
||||
});
|
||||
|
||||
expect(mapped).to.eql([
|
||||
['java', 'node'],
|
||||
['java', 'python', 'node'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct trace samples for join sequences', async () => {
|
||||
const {
|
||||
body: { samples },
|
||||
} = await fetchTraceSamples({
|
||||
query: `sequence by trace.id
|
||||
[ span where service.name == "java" ] by span.id
|
||||
[ transaction where service.name == "python" ] by parent.id`,
|
||||
type: TraceSearchType.eql,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
});
|
||||
|
||||
const traces = await fetchTraces(samples);
|
||||
|
||||
expect(traces.length).to.eql(1);
|
||||
|
||||
const mapped = traces.map((traceDocs) => {
|
||||
return sortBy(traceDocs, '@timestamp')
|
||||
.filter((doc) => doc.processor.event === 'transaction')
|
||||
.map((doc) => doc.service.name);
|
||||
});
|
||||
|
||||
expect(mapped).to.eql([['java', 'python', 'node']]);
|
||||
});
|
||||
|
||||
it('returns the correct trace samples for exit spans', async () => {
|
||||
const {
|
||||
body: { samples },
|
||||
} = await fetchTraceSamples({
|
||||
query: `sequence by trace.id
|
||||
[ transaction where service.name == "python" ]
|
||||
[ span where span.destination.service.resource == "redis" ]`,
|
||||
type: TraceSearchType.eql,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
});
|
||||
|
||||
const traces = await fetchTraces(samples);
|
||||
|
||||
expect(traces.length).to.eql(1);
|
||||
|
||||
const mapped = traces.map((traceDocs) => {
|
||||
return sortBy(traceDocs, '@timestamp')
|
||||
.filter(
|
||||
(doc) => doc.processor.event === 'transaction' || doc.processor.event === 'span'
|
||||
)
|
||||
.map((doc) => {
|
||||
if (doc.span && 'destination' in doc.span) {
|
||||
return doc.span.destination!.service.resource;
|
||||
}
|
||||
return doc.service.name;
|
||||
});
|
||||
});
|
||||
|
||||
expect(mapped).to.eql([['python', 'redis']]);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue