[APM] Link Transaction to Infra & Logging with action menu (#27291) (#27445)

* [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:
Oliver Gupte 2018-12-18 15:24:16 -08:00 committed by GitHub
parent dd9e773156
commit 9a7bce69f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 955 additions and 174 deletions

View file

@ -106,6 +106,14 @@ describe('Transaction v2', () => {
result: 'transaction result',
sampled: true,
type: 'transaction type'
},
kubernetes: {
pod: {
uid: 'pod1234567890abcdef'
}
},
container: {
id: 'container1234567890abcdef'
}
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": ""
}
}

View file

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

View file

@ -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} />;
};

View file

@ -91,6 +91,14 @@ export interface TransactionV2 extends APMDocV2 {
};
type: string;
};
kubernetes: {
pod: {
uid: string;
};
};
container: {
id: string;
};
}
export type Transaction = TransactionV1 | TransactionV2;