mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* [APM] fixes #26574 by adding an Action menu which links a transaction to the Infra & Logging UIs * [APM] - remove link to host metrics filterd by trace id until supported - replace ts-optchain with _.get - remove old, unreferenced ActionMenu dead code - add unit tests * [APM] Make sure the apm index pattern filter is passed thru via the Discover link * [APM] refactored DiscoverButton and TransactionActionMenu to use new QueryWithIndexPattern component to pass along the correct index pattern via the discover link
This commit is contained in:
parent
dd9e773156
commit
9a7bce69f1
17 changed files with 955 additions and 174 deletions
|
@ -106,6 +106,14 @@ describe('Transaction v2', () => {
|
|||
result: 'transaction result',
|
||||
sampled: true,
|
||||
type: 'transaction type'
|
||||
},
|
||||
kubernetes: {
|
||||
pod: {
|
||||
uid: 'pod1234567890abcdef'
|
||||
}
|
||||
},
|
||||
container: {
|
||||
id: 'container1234567890abcdef'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { KibanaLink } from 'x-pack/plugins/apm/public/utils/url';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import { DiscoverTransactionButton } from '../../../shared/DiscoverButtons/DiscoverTransactionButton';
|
||||
|
||||
function getInfraMetricsQuery(transaction: Transaction) {
|
||||
const plus5 = new Date(transaction['@timestamp']);
|
||||
const minus5 = new Date(transaction['@timestamp']);
|
||||
|
||||
plus5.setMinutes(plus5.getMinutes() + 5);
|
||||
minus5.setMinutes(minus5.getMinutes() - 5);
|
||||
|
||||
return {
|
||||
from: minus5.getTime(),
|
||||
to: plus5.getTime()
|
||||
};
|
||||
}
|
||||
|
||||
function ActionMenuButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" onClick={onClick}>
|
||||
Actions
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActionMenuProps {
|
||||
readonly transaction: Transaction;
|
||||
}
|
||||
|
||||
interface ActionMenuState {
|
||||
readonly isOpen: boolean;
|
||||
}
|
||||
|
||||
export class ActionMenu extends React.Component<
|
||||
ActionMenuProps,
|
||||
ActionMenuState
|
||||
> {
|
||||
public state = {
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
public toggle = () => {
|
||||
this.setState(state => ({ isOpen: !state.isOpen }));
|
||||
};
|
||||
|
||||
public close = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
public getInfraActions(transaction: Transaction) {
|
||||
const { system } = transaction.context;
|
||||
|
||||
if (!system || !system.hostname) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem icon="infraApp" key="infra-host-metrics">
|
||||
<KibanaLink
|
||||
pathname="/app/infra"
|
||||
hash={`/link-to/host-detail/${system.hostname}`}
|
||||
query={getInfraMetricsQuery(transaction)}
|
||||
>
|
||||
<span>View host metrics (beta)</span>
|
||||
</KibanaLink>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem icon="infraApp" key="infra-host-logs">
|
||||
<KibanaLink
|
||||
pathname="/app/infra"
|
||||
hash={`/link-to/host-logs/${system.hostname}`}
|
||||
query={{ time: new Date(transaction['@timestamp']).getTime() }}
|
||||
>
|
||||
<span>View host logs (beta)</span>
|
||||
</KibanaLink>
|
||||
</EuiContextMenuItem>
|
||||
];
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { transaction } = this.props;
|
||||
|
||||
const items = [
|
||||
<EuiContextMenuItem icon="discoverApp" key="discover-transaction">
|
||||
<DiscoverTransactionButton transaction={transaction}>
|
||||
View sample document
|
||||
</DiscoverTransactionButton>
|
||||
</EuiContextMenuItem>,
|
||||
...this.getInfraActions(transaction)
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="transactionActionMenu"
|
||||
button={<ActionMenuButton onClick={this.toggle} />}
|
||||
isOpen={this.state.isOpen}
|
||||
closePopover={this.close}
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} title="Actions" />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -20,7 +19,7 @@ import {
|
|||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DiscoverTransactionButton } from 'x-pack/plugins/apm/public/components/shared/DiscoverButtons/DiscoverTransactionButton';
|
||||
import { TransactionActionMenu } from 'x-pack/plugins/apm/public/components/shared/TransactionActionMenu/TransactionActionMenu';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
import { APM_AGENT_DROPPED_SPANS_DOCS } from 'x-pack/plugins/apm/public/utils/documentation/agents';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
|
@ -115,11 +114,10 @@ export function TransactionFlyout({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<DiscoverTransactionButton transaction={transactionDoc}>
|
||||
<EuiButtonEmpty iconType="discoverApp">
|
||||
View transaction in Discover
|
||||
</EuiButtonEmpty>
|
||||
</DiscoverTransactionButton>
|
||||
<TransactionActionMenu
|
||||
transaction={transactionDoc}
|
||||
location={location}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
|
@ -17,7 +16,7 @@ import {
|
|||
import React from 'react';
|
||||
import { Transaction as ITransaction } from '../../../../../typings/es_schemas/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { DiscoverTransactionButton } from '../../../shared/DiscoverButtons/DiscoverTransactionButton';
|
||||
import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu';
|
||||
import { TransactionLink } from '../../../shared/TransactionLink';
|
||||
import { StickyTransactionProperties } from './StickyTransactionProperties';
|
||||
import { TransactionPropertiesTable } from './TransactionPropertiesTable';
|
||||
|
@ -95,11 +94,10 @@ export const Transaction: React.SFC<Props> = ({
|
|||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DiscoverTransactionButton transaction={transaction}>
|
||||
<EuiButtonEmpty iconType="discoverApp">
|
||||
View transaction in Discover
|
||||
</EuiButtonEmpty>
|
||||
</DiscoverTransactionButton>
|
||||
<TransactionActionMenu
|
||||
transaction={transaction}
|
||||
location={location}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<MaybeViewTraceLink
|
||||
transaction={transaction}
|
||||
|
|
|
@ -6,43 +6,28 @@
|
|||
|
||||
import React from 'react';
|
||||
import { StringMap } from 'x-pack/plugins/apm/typings/common';
|
||||
import {
|
||||
getAPMIndexPattern,
|
||||
ISavedObject
|
||||
} from '../../../services/rest/savedObjects';
|
||||
import { KibanaLink } from '../../../utils/url';
|
||||
import { QueryWithIndexPattern } from './QueryWithIndexPattern';
|
||||
|
||||
interface Props {
|
||||
query: StringMap;
|
||||
}
|
||||
|
||||
interface State {
|
||||
indexPattern?: ISavedObject;
|
||||
}
|
||||
|
||||
export class DiscoverButton extends React.Component<Props, State> {
|
||||
public state: State = {};
|
||||
public async componentDidMount() {
|
||||
const indexPattern = await getAPMIndexPattern();
|
||||
this.setState({ indexPattern });
|
||||
}
|
||||
|
||||
export class DiscoverButton extends React.Component<Props> {
|
||||
public render() {
|
||||
const { query, children, ...rest } = this.props;
|
||||
const id = this.state.indexPattern && this.state.indexPattern.id;
|
||||
|
||||
if (!query._a.index) {
|
||||
query._a.index = id;
|
||||
}
|
||||
|
||||
return (
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={'/discover'}
|
||||
query={query}
|
||||
children={children}
|
||||
{...rest}
|
||||
/>
|
||||
<QueryWithIndexPattern query={query}>
|
||||
{queryWithIndexPattern => (
|
||||
<KibanaLink
|
||||
pathname={'/app/kibana'}
|
||||
hash={'/discover'}
|
||||
query={queryWithIndexPattern}
|
||||
children={children}
|
||||
{...rest}
|
||||
/>
|
||||
)}
|
||||
</QueryWithIndexPattern>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,11 @@ import {
|
|||
TRACE_ID,
|
||||
TRANSACTION_ID
|
||||
} from 'x-pack/plugins/apm/common/constants';
|
||||
import { StringMap } from 'x-pack/plugins/apm/typings/common';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import { DiscoverButton } from './DiscoverButton';
|
||||
|
||||
function getDiscoverQuery(transaction: Transaction) {
|
||||
export function getDiscoverQuery(transaction: Transaction): StringMap {
|
||||
const transactionId = transaction.transaction.id;
|
||||
const traceId =
|
||||
transaction.version === 'v2' ? transaction.trace.id : undefined;
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import { StringMap } from 'x-pack/plugins/apm/typings/common';
|
||||
import {
|
||||
getAPMIndexPattern,
|
||||
ISavedObject
|
||||
} from '../../../services/rest/savedObjects';
|
||||
|
||||
export function getQueryWithIndexPattern(
|
||||
query: StringMap = {},
|
||||
indexPattern?: ISavedObject
|
||||
) {
|
||||
if ((query._a && query._a.index) || !indexPattern) {
|
||||
return query;
|
||||
}
|
||||
|
||||
const id = indexPattern && indexPattern.id;
|
||||
|
||||
return {
|
||||
...query,
|
||||
_a: {
|
||||
...query._a,
|
||||
index: id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
query?: StringMap;
|
||||
children: (query: StringMap) => ReactElement<any>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
indexPattern?: ISavedObject;
|
||||
}
|
||||
|
||||
export class QueryWithIndexPattern extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
getAPMIndexPattern().then(indexPattern => {
|
||||
this.setState({ indexPattern });
|
||||
});
|
||||
this.state = {};
|
||||
}
|
||||
public render() {
|
||||
const { children, query } = this.props;
|
||||
const { indexPattern } = this.state;
|
||||
const renderWithQuery = children;
|
||||
return renderWithQuery(getQueryWithIndexPattern(query, indexPattern));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import 'jest-styled-components';
|
||||
import React from 'react';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import {
|
||||
DiscoverTransactionButton,
|
||||
getDiscoverQuery
|
||||
} from '../DiscoverTransactionButton';
|
||||
import mockTransaction from './mockTransaction.json';
|
||||
|
||||
describe('DiscoverTransactionButton component', () => {
|
||||
it('should render with data', () => {
|
||||
const transaction: Transaction = mockTransaction;
|
||||
|
||||
expect(
|
||||
shallow(<DiscoverTransactionButton transaction={transaction} />)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDiscoverQuery', () => {
|
||||
it('should return the correct query params object', () => {
|
||||
const transaction: Transaction = mockTransaction;
|
||||
const result = getDiscoverQuery(transaction);
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DiscoverTransactionButton component should render with data 1`] = `
|
||||
<DiscoverButton
|
||||
query={
|
||||
Object {
|
||||
"_a": Object {
|
||||
"interval": "auto",
|
||||
"query": Object {
|
||||
"language": "lucene",
|
||||
"query": "processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`getDiscoverQuery should return the correct query params object 1`] = `
|
||||
Object {
|
||||
"_a": Object {
|
||||
"interval": "auto",
|
||||
"query": Object {
|
||||
"language": "lucene",
|
||||
"query": "processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,115 @@
|
|||
{
|
||||
"agent": {
|
||||
"hostname": "227453131a17",
|
||||
"type": "apm-server",
|
||||
"version": "7.0.0"
|
||||
},
|
||||
"processor": {
|
||||
"name": "transaction",
|
||||
"event": "transaction"
|
||||
},
|
||||
"trace": {
|
||||
"id": "8b60bd32ecc6e1506735a8b6cfcf175c"
|
||||
},
|
||||
"@timestamp": "2018-12-18T00:14:30.952Z",
|
||||
"host": {
|
||||
"name": "227453131a17"
|
||||
},
|
||||
"context": {
|
||||
"request": {
|
||||
"headers": {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Python/3.7 aiohttp/3.3.2",
|
||||
"Accept-Encoding": "gzip, deflate"
|
||||
},
|
||||
"method": "GET",
|
||||
"http_version": "1.1",
|
||||
"socket": {
|
||||
"remote_address": "172.18.0.12"
|
||||
},
|
||||
"url": {
|
||||
"protocol": "http",
|
||||
"hostname": "172.18.0.7",
|
||||
"port": "3000",
|
||||
"full": "http://172.18.0.7:3000/api/products/3/customers",
|
||||
"pathname": "/api/products/3/customers"
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"pid": 1,
|
||||
"title": "opbeans-go",
|
||||
"argv": [
|
||||
"/opbeans-go",
|
||||
"-listen=:3000",
|
||||
"-frontend=/opbeans-frontend",
|
||||
"-db=postgres:",
|
||||
"-cache=redis://redis:6379"
|
||||
],
|
||||
"ppid": 0
|
||||
},
|
||||
"system": {
|
||||
"hostname": "8acb9c1a71f3",
|
||||
"ip": "172.18.0.7",
|
||||
"platform": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"response": {
|
||||
"headers": {
|
||||
"X-Frame-Options": "SAMEORIGIN",
|
||||
"Server": "gunicorn/19.9.0",
|
||||
"Vary": "Cookie",
|
||||
"Content-Length": "31646",
|
||||
"Date": "Tue, 18 Dec 2018 00:14:45 GMT",
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
},
|
||||
"status_code": 200
|
||||
},
|
||||
"service": {
|
||||
"agent": {
|
||||
"name": "go",
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"framework": {
|
||||
"name": "gin",
|
||||
"version": "v1.4.0-dev"
|
||||
},
|
||||
"name": "opbeans-go",
|
||||
"runtime": {
|
||||
"name": "gc",
|
||||
"version": "go1.10.6"
|
||||
},
|
||||
"language": {
|
||||
"name": "go",
|
||||
"version": "go1.10.6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transaction": {
|
||||
"result": "HTTP 2xx",
|
||||
"duration": {
|
||||
"us": 14586403
|
||||
},
|
||||
"name": "GET /api/products/:id/customers",
|
||||
"span_count": {
|
||||
"dropped": {
|
||||
"total": 0
|
||||
},
|
||||
"started": 1
|
||||
},
|
||||
"id": "8b60bd32ecc6e150",
|
||||
"type": "request",
|
||||
"sampled": true
|
||||
},
|
||||
"kubernetes": {
|
||||
"pod": {
|
||||
"uid": "pod123456abcdef"
|
||||
}
|
||||
},
|
||||
"container": {
|
||||
"id": "container123456abcdef"
|
||||
},
|
||||
"timestamp": {
|
||||
"us": 1545092070952472
|
||||
},
|
||||
"version": "v2"
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiLink,
|
||||
EuiPopover
|
||||
} from '@elastic/eui';
|
||||
import { get } from 'lodash';
|
||||
import React from 'react';
|
||||
import { getKibanaHref } from 'x-pack/plugins/apm/public/utils/url';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import { getDiscoverQuery } from '../DiscoverButtons/DiscoverTransactionButton';
|
||||
import { QueryWithIndexPattern } from '../DiscoverButtons/QueryWithIndexPattern';
|
||||
|
||||
function getInfraMetricsQuery(transaction: Transaction) {
|
||||
const plus5 = new Date(transaction['@timestamp']);
|
||||
const minus5 = new Date(transaction['@timestamp']);
|
||||
|
||||
plus5.setMinutes(plus5.getMinutes() + 5);
|
||||
minus5.setMinutes(minus5.getMinutes() - 5);
|
||||
|
||||
return {
|
||||
from: minus5.getTime(),
|
||||
to: plus5.getTime()
|
||||
};
|
||||
}
|
||||
|
||||
function ActionMenuButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
<EuiButtonEmpty iconType="arrowDown" iconSide="right" onClick={onClick}>
|
||||
Actions
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
readonly transaction: Transaction;
|
||||
readonly location: Location;
|
||||
}
|
||||
|
||||
interface State {
|
||||
readonly isOpen: boolean;
|
||||
}
|
||||
|
||||
export class TransactionActionMenu extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
isOpen: false
|
||||
};
|
||||
|
||||
public toggle = () => {
|
||||
this.setState(state => ({ isOpen: !state.isOpen }));
|
||||
};
|
||||
|
||||
public close = () => {
|
||||
this.setState({ isOpen: false });
|
||||
};
|
||||
|
||||
public getInfraActions(transaction: Transaction) {
|
||||
const hostName = get(transaction, 'context.system.hostname');
|
||||
const podId = get(transaction, 'kubernetes.pod.uid');
|
||||
const containerId = get(transaction, 'container.id');
|
||||
const pathname = '/app/infra';
|
||||
const time = new Date(transaction['@timestamp']).getTime();
|
||||
const infraMetricsQuery = getInfraMetricsQuery(transaction);
|
||||
|
||||
return [
|
||||
{
|
||||
icon: 'loggingApp',
|
||||
label: 'Show pod logs',
|
||||
target: podId,
|
||||
hash: `/link-to/pod-logs/${podId}`,
|
||||
query: { time }
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'loggingApp',
|
||||
label: 'Show container logs',
|
||||
target: containerId,
|
||||
hash: `/link-to/container-logs/${containerId}`,
|
||||
query: { time }
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'loggingApp',
|
||||
label: 'Show host logs',
|
||||
target: hostName,
|
||||
hash: `/link-to/host-logs/${hostName}`,
|
||||
query: { time }
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'infraApp',
|
||||
label: 'Show pod metrics',
|
||||
target: podId,
|
||||
hash: `/link-to/pod-detail/${podId}`,
|
||||
query: infraMetricsQuery
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'infraApp',
|
||||
label: 'Show container metrics',
|
||||
target: containerId,
|
||||
hash: `/link-to/container-detail/${containerId}`,
|
||||
query: infraMetricsQuery
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'infraApp',
|
||||
label: 'Show host metrics',
|
||||
target: hostName,
|
||||
hash: `/link-to/host-detail/${hostName}`,
|
||||
query: infraMetricsQuery
|
||||
}
|
||||
]
|
||||
.filter(({ target }) => Boolean(target))
|
||||
.map(({ icon, label, hash, query }, index) => {
|
||||
const href = getKibanaHref({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiContextMenuItem icon={icon} href={href} key={index}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiLink>{label}</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="popout" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { transaction, location } = this.props;
|
||||
return (
|
||||
<QueryWithIndexPattern query={getDiscoverQuery(transaction)}>
|
||||
{query => {
|
||||
const discoverTransactionHref = getKibanaHref({
|
||||
location,
|
||||
pathname: '/app/kibana',
|
||||
hash: '/discover',
|
||||
query
|
||||
});
|
||||
|
||||
const items = [
|
||||
...this.getInfraActions(transaction),
|
||||
<EuiContextMenuItem
|
||||
icon="discoverApp"
|
||||
href={discoverTransactionHref}
|
||||
key="discover-transaction"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiLink>View sample document</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="popout" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="transactionActionMenu"
|
||||
button={<ActionMenuButton onClick={this.toggle} />}
|
||||
isOpen={this.state.isOpen}
|
||||
closePopover={this.close}
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} title="Actions" />
|
||||
</EuiPopover>
|
||||
);
|
||||
}}
|
||||
</QueryWithIndexPattern>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
import 'jest-styled-components';
|
||||
import React from 'react';
|
||||
import { Transaction } from 'x-pack/plugins/apm/typings/es_schemas/Transaction';
|
||||
import { TransactionActionMenu } from '../TransactionActionMenu';
|
||||
import transactionActionMenuProps from './transactionActionMenuProps.json';
|
||||
|
||||
describe('TransactionActionMenu component', () => {
|
||||
it('should render with data', () => {
|
||||
const transaction: Transaction = transactionActionMenuProps.transaction;
|
||||
const location: Location = transactionActionMenuProps.location;
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<TransactionActionMenu transaction={transaction} location={location} />
|
||||
).shallow()
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,286 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TransactionActionMenu component should render with data 1`] = `
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
button={
|
||||
<ActionMenuButton
|
||||
onClick={[Function]}
|
||||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
hasArrow={true}
|
||||
id="transactionActionMenu"
|
||||
isOpen={false}
|
||||
ownFocus={false}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel
|
||||
hasFocus={true}
|
||||
items={
|
||||
Array [
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/pod-logs/pod123456abcdef?time=1545092070952&_g=(time:(from:now-24h,mode:quick,to:now))&_a="
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show pod logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/container-logs/container123456abcdef?time=1545092070952&_g=(time:(from:now-24h,mode:quick,to:now))&_a="
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show container logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/host-logs/8acb9c1a71f3?time=1545092070952&_g=(time:(from:now-24h,mode:quick,to:now))&_a="
|
||||
icon="loggingApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show host logs
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/pod-detail/pod123456abcdef?from=1545091770952&to=1545092370952&_g=(time:(from:now-24h,mode:quick,to:now))&_a="
|
||||
icon="infraApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show pod metrics
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/container-detail/container123456abcdef?from=1545091770952&to=1545092370952&_g=(time:(from:now-24h,mode:quick,to:now))&_a="
|
||||
icon="infraApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show container metrics
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/infra#/link-to/host-detail/8acb9c1a71f3?from=1545091770952&to=1545092370952&_g=(time:(from:now-24h,mode:quick,to:now))&_a="
|
||||
icon="infraApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
Show host metrics
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
href="/app/kibana#/discover?_a=(interval:auto,query:(language:lucene,query:'processor.event:\\"transaction\\" AND transaction.id:\\"8b60bd32ecc6e150\\" AND trace.id:\\"8b60bd32ecc6e1506735a8b6cfcf175c\\"'))&_g=(time:(from:now-24h,mode:quick,to:now))"
|
||||
icon="discoverApp"
|
||||
layoutAlign="center"
|
||||
toolTipPosition="right"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
alignItems="stretch"
|
||||
component="div"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
justifyContent="flexStart"
|
||||
responsive={true}
|
||||
wrap={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={true}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
type="button"
|
||||
>
|
||||
View sample document
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
component="div"
|
||||
grow={false}
|
||||
>
|
||||
<EuiIcon
|
||||
size="m"
|
||||
type="popout"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiContextMenuItem>,
|
||||
]
|
||||
}
|
||||
title="Actions"
|
||||
/>
|
||||
</EuiPopover>
|
||||
`;
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"transaction": {
|
||||
"agent": {
|
||||
"hostname": "227453131a17",
|
||||
"type": "apm-server",
|
||||
"version": "7.0.0"
|
||||
},
|
||||
"processor": {
|
||||
"name": "transaction",
|
||||
"event": "transaction"
|
||||
},
|
||||
"trace": {
|
||||
"id": "8b60bd32ecc6e1506735a8b6cfcf175c"
|
||||
},
|
||||
"@timestamp": "2018-12-18T00:14:30.952Z",
|
||||
"host": {
|
||||
"name": "227453131a17"
|
||||
},
|
||||
"context": {
|
||||
"request": {
|
||||
"headers": {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Python/3.7 aiohttp/3.3.2",
|
||||
"Accept-Encoding": "gzip, deflate"
|
||||
},
|
||||
"method": "GET",
|
||||
"http_version": "1.1",
|
||||
"socket": {
|
||||
"remote_address": "172.18.0.12"
|
||||
},
|
||||
"url": {
|
||||
"protocol": "http",
|
||||
"hostname": "172.18.0.7",
|
||||
"port": "3000",
|
||||
"full": "http://172.18.0.7:3000/api/products/3/customers",
|
||||
"pathname": "/api/products/3/customers"
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"pid": 1,
|
||||
"title": "opbeans-go",
|
||||
"argv": [
|
||||
"/opbeans-go",
|
||||
"-listen=:3000",
|
||||
"-frontend=/opbeans-frontend",
|
||||
"-db=postgres:",
|
||||
"-cache=redis://redis:6379"
|
||||
],
|
||||
"ppid": 0
|
||||
},
|
||||
"system": {
|
||||
"hostname": "8acb9c1a71f3",
|
||||
"ip": "172.18.0.7",
|
||||
"platform": "linux",
|
||||
"architecture": "amd64"
|
||||
},
|
||||
"response": {
|
||||
"headers": {
|
||||
"X-Frame-Options": "SAMEORIGIN",
|
||||
"Server": "gunicorn/19.9.0",
|
||||
"Vary": "Cookie",
|
||||
"Content-Length": "31646",
|
||||
"Date": "Tue, 18 Dec 2018 00:14:45 GMT",
|
||||
"Content-Type": "application/json; charset=utf-8"
|
||||
},
|
||||
"status_code": 200
|
||||
},
|
||||
"service": {
|
||||
"agent": {
|
||||
"name": "go",
|
||||
"version": "1.1.1"
|
||||
},
|
||||
"framework": {
|
||||
"name": "gin",
|
||||
"version": "v1.4.0-dev"
|
||||
},
|
||||
"name": "opbeans-go",
|
||||
"runtime": {
|
||||
"name": "gc",
|
||||
"version": "go1.10.6"
|
||||
},
|
||||
"language": {
|
||||
"name": "go",
|
||||
"version": "go1.10.6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transaction": {
|
||||
"result": "HTTP 2xx",
|
||||
"duration": {
|
||||
"us": 14586403
|
||||
},
|
||||
"name": "GET /api/products/:id/customers",
|
||||
"span_count": {
|
||||
"dropped": {
|
||||
"total": 0
|
||||
},
|
||||
"started": 1
|
||||
},
|
||||
"id": "8b60bd32ecc6e150",
|
||||
"type": "request",
|
||||
"sampled": true
|
||||
},
|
||||
"kubernetes": {
|
||||
"pod": {
|
||||
"uid": "pod123456abcdef"
|
||||
}
|
||||
},
|
||||
"container": {
|
||||
"id": "container123456abcdef"
|
||||
},
|
||||
"timestamp": {
|
||||
"us": 1545092070952472
|
||||
},
|
||||
"version": "v2"
|
||||
},
|
||||
"location": {
|
||||
"pathname": "/opbeans-go/transactions/request/GET~20~2Fapi~2Fproducts~2F~3Aid~2Fcustomers",
|
||||
"search": "?_g=()&flyoutDetailTab=undefined&waterfallItemId=8b60bd32ecc6e150",
|
||||
"hash": ""
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import url from 'url';
|
|||
import { toJson } from '../testHelpers';
|
||||
import {
|
||||
fromQuery,
|
||||
getKibanaHref,
|
||||
RelativeLinkComponent,
|
||||
toQuery,
|
||||
UnconnectedKibanaLink,
|
||||
|
@ -105,6 +106,35 @@ describe('RelativeLinkComponent', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getKibanaHref', () => {
|
||||
it('should return the correct href', () => {
|
||||
const href = getKibanaHref({
|
||||
location: { search: '' },
|
||||
pathname: '/app/kibana',
|
||||
hash: '/discover',
|
||||
query: {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: `context.service.name:"myServiceName" AND error.grouping_key:"myGroupId"`
|
||||
},
|
||||
sort: { '@timestamp': 'desc' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { _g, _a } = getUrlQuery(href);
|
||||
const { pathname } = url.parse(href);
|
||||
|
||||
expect(pathname).toBe('/app/kibana');
|
||||
expect(_a).toBe(
|
||||
'(interval:auto,query:(language:lucene,query:\'context.service.name:"myServiceName" AND error.grouping_key:"myGroupId"\'),sort:(\'@timestamp\':desc))'
|
||||
);
|
||||
expect(_g).toBe('(time:(from:now-24h,mode:quick,to:now))');
|
||||
});
|
||||
});
|
||||
|
||||
function getUnconnectedKibanLink() {
|
||||
const discoverQuery = {
|
||||
_a: {
|
||||
|
|
|
@ -142,6 +142,25 @@ export function RelativeLinkComponent({
|
|||
);
|
||||
}
|
||||
|
||||
export function getKibanaHref(kibanaLinkArgs: KibanaLinkArgs): string {
|
||||
const { location, pathname, hash, query = {} } = kibanaLinkArgs;
|
||||
// Preserve current _g and _a
|
||||
const currentQuery = toQuery(location.search);
|
||||
const g = decodeAndMergeG(currentQuery._g, query._g);
|
||||
const nextQuery = {
|
||||
...query,
|
||||
_g: rison.encode(g),
|
||||
_a: query._a ? rison.encode(query._a) : ''
|
||||
};
|
||||
|
||||
const search = stringifyWithoutEncoding(nextQuery);
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath(pathname),
|
||||
hash: `${hash}?${search}`
|
||||
});
|
||||
return href;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// Both KibanaLink and RelativeLink does similar things, are too magic, and have different APIs.
|
||||
// The initial idea with KibanaLink was to automatically preserve the timestamp (_g) when making links. RelativeLink went a bit overboard and preserves all query args
|
||||
|
@ -178,21 +197,12 @@ export const UnconnectedKibanaLink: React.SFC<KibanaLinkArgs> = ({
|
|||
query = {},
|
||||
...props
|
||||
}) => {
|
||||
// Preserve current _g and _a
|
||||
const currentQuery = toQuery(location.search);
|
||||
const g = decodeAndMergeG(currentQuery._g, query._g);
|
||||
const nextQuery = {
|
||||
...query,
|
||||
_g: rison.encode(g),
|
||||
_a: query._a ? rison.encode(query._a) : ''
|
||||
};
|
||||
|
||||
const search = stringifyWithoutEncoding(nextQuery);
|
||||
const href = url.format({
|
||||
pathname: chrome.addBasePath(pathname),
|
||||
hash: `${hash}?${search}`
|
||||
const href = getKibanaHref({
|
||||
location,
|
||||
pathname,
|
||||
hash,
|
||||
query
|
||||
});
|
||||
|
||||
return <EuiLink {...props} href={href} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -91,6 +91,14 @@ export interface TransactionV2 extends APMDocV2 {
|
|||
};
|
||||
type: string;
|
||||
};
|
||||
kubernetes: {
|
||||
pod: {
|
||||
uid: string;
|
||||
};
|
||||
};
|
||||
container: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Transaction = TransactionV1 | TransactionV2;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue