mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] Storybook support for the new service map API response (#213980)
closes [213126](https://github.com/elastic/kibana/issues/213126) ## Summary Add support for the new API response to the Service Map storybook 
This commit is contained in:
parent
84ae9c98c2
commit
4502a930d5
5 changed files with 291 additions and 121 deletions
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 type { ConnectionNode, ExitSpanDestination, ServiceMapSpan } from './types';
|
||||
import { getConnections, getExternalConnectionNode, getServiceConnectionNode } from './utils';
|
||||
|
||||
export const getPaths = ({ spans }: { spans: ServiceMapSpan[] }) => {
|
||||
const connections: ConnectionNode[][] = [];
|
||||
const exitSpanDestinations: ExitSpanDestination[] = [];
|
||||
|
||||
for (const currentNode of spans) {
|
||||
const exitSpanNode = getExternalConnectionNode(currentNode);
|
||||
const serviceNode = getServiceConnectionNode(currentNode);
|
||||
|
||||
if (currentNode.destinationService) {
|
||||
// maps an exit span to its destination service
|
||||
exitSpanDestinations.push({
|
||||
from: exitSpanNode,
|
||||
to: getServiceConnectionNode(currentNode.destinationService),
|
||||
});
|
||||
}
|
||||
|
||||
// builds a connection between a service and an exit span
|
||||
connections.push([serviceNode, exitSpanNode]);
|
||||
}
|
||||
|
||||
return {
|
||||
connections: getConnections(connections),
|
||||
exitSpanDestinations,
|
||||
};
|
||||
};
|
|
@ -24,6 +24,7 @@ import type {
|
|||
|
||||
export * from './utils';
|
||||
export { getServiceMapNodes } from './get_service_map_nodes';
|
||||
export { getPaths } from './get_paths';
|
||||
|
||||
export type {
|
||||
Connection,
|
||||
|
|
|
@ -7,17 +7,23 @@
|
|||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCallOut,
|
||||
EuiFieldNumber,
|
||||
EuiFilePicker,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiSpacer,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import type { Meta, Story } from '@storybook/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { CodeEditor } from '@kbn/code-editor';
|
||||
import { useArgs } from '@storybook/addons';
|
||||
import {
|
||||
getPaths,
|
||||
getServiceMapNodes,
|
||||
type ServiceMapResponse,
|
||||
} from '../../../../../common/service_map';
|
||||
import { Cytoscape } from '../cytoscape';
|
||||
import { Centerer } from './centerer';
|
||||
import exampleResponseHipsterStore from './example_response_hipster_store.json';
|
||||
|
@ -26,36 +32,16 @@ import exampleResponseTodo from './example_response_todo.json';
|
|||
import { generateServiceMapElements } from './generate_service_map_elements';
|
||||
import { MockApmPluginStorybook } from '../../../../context/apm_plugin/mock_apm_plugin_storybook';
|
||||
|
||||
const STORYBOOK_PATH = 'app/ServiceMap/Example data';
|
||||
|
||||
const SESSION_STORAGE_KEY = `${STORYBOOK_PATH}/pre-loaded map`;
|
||||
function getSessionJson() {
|
||||
return window.sessionStorage.getItem(SESSION_STORAGE_KEY);
|
||||
}
|
||||
function setSessionJson(json: string) {
|
||||
window.sessionStorage.setItem(SESSION_STORAGE_KEY, json);
|
||||
}
|
||||
|
||||
function getHeight() {
|
||||
return window.innerHeight - 300;
|
||||
return window.innerHeight - 200;
|
||||
}
|
||||
|
||||
const stories: Meta<{}> = {
|
||||
title: 'app/ServiceMap/Example data',
|
||||
component: Cytoscape,
|
||||
decorators: [
|
||||
(StoryComponent, { globals }) => {
|
||||
return (
|
||||
<MockApmPluginStorybook>
|
||||
<StoryComponent />
|
||||
</MockApmPluginStorybook>
|
||||
);
|
||||
},
|
||||
],
|
||||
decorators: [(wrappedStory) => <MockApmPluginStorybook>{wrappedStory()}</MockApmPluginStorybook>],
|
||||
};
|
||||
|
||||
export default stories;
|
||||
|
||||
export const GenerateMap: Story<{}> = () => {
|
||||
const [size, setSize] = useState<number>(10);
|
||||
const [json, setJson] = useState<string>('');
|
||||
|
@ -114,87 +100,106 @@ export const GenerateMap: Story<{}> = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const MapFromJSON: Story<{}> = () => {
|
||||
const [json, setJson] = useState<string>(
|
||||
getSessionJson() || JSON.stringify(exampleResponseTodo, null, 2)
|
||||
);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
interface MapFromJSONArgs {
|
||||
json: unknown;
|
||||
}
|
||||
const assertJSON: (json?: any) => asserts json is ServiceMapResponse = (json) => {
|
||||
if (!!json && !('elements' in json || 'spans' in json)) {
|
||||
throw new Error('invalid json');
|
||||
}
|
||||
};
|
||||
|
||||
const MapFromJSONTemplate: Story<MapFromJSONArgs> = (args) => {
|
||||
const [{ json }, updateArgs] = useArgs();
|
||||
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [elements, setElements] = useState<any[]>([]);
|
||||
|
||||
const [uniqueKeyCounter, setUniqueKeyCounter] = useState<number>(0);
|
||||
const updateRenderedElements = () => {
|
||||
const updateRenderedElements = useCallback(() => {
|
||||
try {
|
||||
setElements(JSON.parse(json).elements);
|
||||
assertJSON(json);
|
||||
if ('elements' in json) {
|
||||
setElements(json.elements ?? []);
|
||||
} else {
|
||||
const paths = getPaths({ spans: json.spans ?? [] });
|
||||
const nodes = getServiceMapNodes({
|
||||
anomalies: json.anomalies ?? {
|
||||
mlJobIds: [],
|
||||
serviceAnomalies: [],
|
||||
},
|
||||
connections: paths.connections,
|
||||
servicesData: json.servicesData ?? [],
|
||||
exitSpanDestinations: paths.exitSpanDestinations,
|
||||
});
|
||||
|
||||
setElements(nodes.elements);
|
||||
}
|
||||
setUniqueKeyCounter((key) => key + 1);
|
||||
setError(undefined);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
}, [json]);
|
||||
|
||||
useEffect(() => {
|
||||
updateRenderedElements();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [updateRenderedElements]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Cytoscape key={uniqueKeyCounter} elements={elements} height={getHeight()}>
|
||||
<Centerer />
|
||||
</Cytoscape>
|
||||
<EuiForm isInvalid={error !== undefined} error={error}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<CodeEditor // TODO Unable to find context that provides theme. Need CODEOWNER Input
|
||||
languageId="json"
|
||||
value={json}
|
||||
options={{ fontFamily: 'monospace' }}
|
||||
onChange={(value) => {
|
||||
setJson(value);
|
||||
setSessionJson(value);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFilePicker
|
||||
display={'large'}
|
||||
fullWidth={true}
|
||||
style={{ height: '100%' }}
|
||||
initialPromptText="Upload a JSON file"
|
||||
onChange={(event) => {
|
||||
const item = event?.item(0);
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
justifyContent="spaceBetween"
|
||||
style={{ minHeight: '100vh' }}
|
||||
gutterSize="xs"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
title="Upload a JSON file or paste a JSON object in the Storybook Controls panel."
|
||||
iconType="pin"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<Cytoscape key={uniqueKeyCounter} elements={elements} height={getHeight()}>
|
||||
<Centerer />
|
||||
</Cytoscape>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiForm isInvalid={error !== undefined} error={error}>
|
||||
<EuiFilePicker
|
||||
display="large"
|
||||
fullWidth
|
||||
initialPromptText="Upload a JSON file"
|
||||
onChange={(event) => {
|
||||
const item = event?.item(0);
|
||||
|
||||
if (item) {
|
||||
const f = new FileReader();
|
||||
f.onload = (onloadEvent) => {
|
||||
const result = onloadEvent?.target?.result;
|
||||
if (typeof result === 'string') {
|
||||
setJson(result);
|
||||
}
|
||||
};
|
||||
f.readAsText(item);
|
||||
if (item) {
|
||||
const f = new FileReader();
|
||||
f.onload = (onloadEvent) => {
|
||||
const result = onloadEvent?.target?.result;
|
||||
if (typeof result === 'string') {
|
||||
updateArgs({ json: JSON.parse(result) });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
data-test-subj="apmMapFromJSONRenderJsonButton"
|
||||
onClick={() => {
|
||||
updateRenderedElements();
|
||||
}}
|
||||
>
|
||||
Render JSON
|
||||
</EuiButton>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiForm>
|
||||
</div>
|
||||
};
|
||||
f.readAsText(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiForm>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const MapFromJSON = MapFromJSONTemplate.bind({});
|
||||
MapFromJSON.argTypes = {
|
||||
json: {
|
||||
defaultValue: exampleResponseTodo,
|
||||
control: 'object',
|
||||
},
|
||||
};
|
||||
|
||||
export const TodoApp: Story<{}> = () => {
|
||||
return (
|
||||
<div>
|
||||
|
@ -224,3 +229,5 @@ export const HipsterStore: Story<{}> = () => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default stories;
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
{
|
||||
"spans": [
|
||||
{
|
||||
"spanId": "f0dc50bb2415f74c",
|
||||
"spanDestinationServiceResource": "sqlite/main",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "sqlite",
|
||||
"agentName": "dotnet",
|
||||
"serviceName": "opbeans-dotnet",
|
||||
"serviceEnvironment": "production"
|
||||
},
|
||||
{
|
||||
"spanId": "1577a1c5d072d16a",
|
||||
"spanDestinationServiceResource": "opbeans:3000",
|
||||
"spanType": "external",
|
||||
"spanSubtype": "http",
|
||||
"agentName": "go",
|
||||
"serviceName": "opbeans-go",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"destinationService": {
|
||||
"agentName": "nodejs",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"serviceName": "opbeans-node"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spanId": "6d4272837d04692f",
|
||||
"spanDestinationServiceResource": "postgresql",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "postgresql",
|
||||
"agentName": "go",
|
||||
"serviceName": "opbeans-go",
|
||||
"serviceEnvironment": "opbeans"
|
||||
},
|
||||
{
|
||||
"spanId": "20c6641d5ac8524a",
|
||||
"spanDestinationServiceResource": "opbeans:3000",
|
||||
"spanType": "external",
|
||||
"spanSubtype": "http",
|
||||
"agentName": "java",
|
||||
"serviceName": "opbeans-java",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"destinationService": {
|
||||
"agentName": "nodejs",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"serviceName": "opbeans-node"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spanId": "33393ec7b7fe2a36",
|
||||
"spanDestinationServiceResource": "postgresql/opbeans-java",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "postgresql",
|
||||
"agentName": "java",
|
||||
"serviceName": "opbeans-java",
|
||||
"serviceEnvironment": "opbeans"
|
||||
},
|
||||
{
|
||||
"spanId": "14cd51ab5ee1d79d",
|
||||
"spanDestinationServiceResource": "opbeans:3000",
|
||||
"spanType": "external",
|
||||
"spanSubtype": "http",
|
||||
"agentName": "nodejs",
|
||||
"serviceName": "opbeans-node",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"destinationService": {
|
||||
"agentName": "go",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"serviceName": "opbeans-go"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spanId": "3e9486b0e6d511d8",
|
||||
"spanDestinationServiceResource": "postgresql/opbeans-node",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "postgresql",
|
||||
"agentName": "nodejs",
|
||||
"serviceName": "opbeans-node",
|
||||
"serviceEnvironment": "opbeans"
|
||||
},
|
||||
{
|
||||
"spanId": "794ac250ad50c678",
|
||||
"spanDestinationServiceResource": "redis",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "redis",
|
||||
"agentName": "nodejs",
|
||||
"serviceName": "opbeans-node",
|
||||
"serviceEnvironment": "opbeans"
|
||||
},
|
||||
{
|
||||
"spanId": "6c39d0f33b597a69",
|
||||
"spanDestinationServiceResource": "mysql/opbeans-php",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "mysql",
|
||||
"agentName": "php",
|
||||
"serviceName": "opbeans-php",
|
||||
"serviceEnvironment": "opbeans"
|
||||
},
|
||||
{
|
||||
"spanId": "e97cd17b6bd9ffe1",
|
||||
"spanDestinationServiceResource": "opbeans:3000",
|
||||
"spanType": "external",
|
||||
"spanSubtype": "http",
|
||||
"agentName": "php",
|
||||
"serviceName": "opbeans-php",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"destinationService": {
|
||||
"agentName": "go",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"serviceName": "opbeans-go"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spanId": "7017bb724934eb14",
|
||||
"spanDestinationServiceResource": "opbeans:3000",
|
||||
"spanType": "external",
|
||||
"spanSubtype": "http",
|
||||
"agentName": "python",
|
||||
"serviceName": "opbeans-python",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"destinationService": {
|
||||
"agentName": "go",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"serviceName": "opbeans-go"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spanId": "0b737a7eb3a2d44c",
|
||||
"spanDestinationServiceResource": "postgresql",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "postgresql",
|
||||
"agentName": "python",
|
||||
"serviceName": "opbeans-python",
|
||||
"serviceEnvironment": "opbeans"
|
||||
},
|
||||
{
|
||||
"spanId": "042d9505225a6995",
|
||||
"spanDestinationServiceResource": "opbeans:3000",
|
||||
"spanType": "external",
|
||||
"spanSubtype": "http",
|
||||
"agentName": "ruby",
|
||||
"serviceName": "opbeans-ruby",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"destinationService": {
|
||||
"agentName": "go",
|
||||
"serviceEnvironment": "opbeans",
|
||||
"serviceName": "opbeans-go"
|
||||
}
|
||||
},
|
||||
{
|
||||
"spanId": "b7708e76d6836ba6",
|
||||
"spanDestinationServiceResource": "postgresql",
|
||||
"spanType": "db",
|
||||
"spanSubtype": "postgresql",
|
||||
"agentName": "ruby",
|
||||
"serviceName": "opbeans-ruby",
|
||||
"serviceEnvironment": "opbeans"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -7,8 +7,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import type {
|
||||
ServiceMapSpan,
|
||||
ExitSpanDestination,
|
||||
ServiceMapRawResponse,
|
||||
ServiceMapTelemetry,
|
||||
} from '../../../../common/service_map/types';
|
||||
|
@ -16,13 +14,8 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_
|
|||
import { useLicenseContext } from '../../../context/license/use_license_context';
|
||||
import { isActivePlatinumLicense } from '../../../../common/license_check';
|
||||
import type { Environment } from '../../../../common/environment_rt';
|
||||
import {
|
||||
getExternalConnectionNode,
|
||||
getServiceConnectionNode,
|
||||
getServiceMapNodes,
|
||||
getConnections,
|
||||
} from '../../../../common/service_map';
|
||||
import type { ConnectionNode, GroupResourceNodesResponse } from '../../../../common/service_map';
|
||||
import { getServiceMapNodes, getPaths } from '../../../../common/service_map';
|
||||
import type { GroupResourceNodesResponse } from '../../../../common/service_map';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
|
||||
type SeriviceMapState = GroupResourceNodesResponse & Pick<ServiceMapTelemetry, 'tracesCount'>;
|
||||
|
@ -142,35 +135,9 @@ export const useServiceMap = ({
|
|||
const processServiceMapData = (data: ServiceMapRawResponse): GroupResourceNodesResponse => {
|
||||
const paths = getPaths({ spans: data.spans });
|
||||
return getServiceMapNodes({
|
||||
connections: getConnections(paths.connections),
|
||||
connections: paths.connections,
|
||||
exitSpanDestinations: paths.exitSpanDestinations,
|
||||
servicesData: data.servicesData,
|
||||
anomalies: data.anomalies,
|
||||
});
|
||||
};
|
||||
|
||||
const getPaths = ({ spans }: { spans: ServiceMapSpan[] }) => {
|
||||
const connections: ConnectionNode[][] = [];
|
||||
const exitSpanDestinations: ExitSpanDestination[] = [];
|
||||
|
||||
for (const currentNode of spans) {
|
||||
const exitSpanNode = getExternalConnectionNode(currentNode);
|
||||
const serviceNode = getServiceConnectionNode(currentNode);
|
||||
|
||||
if (currentNode.destinationService) {
|
||||
// maps an exit span to its destination service
|
||||
exitSpanDestinations.push({
|
||||
from: exitSpanNode,
|
||||
to: getServiceConnectionNode(currentNode.destinationService),
|
||||
});
|
||||
}
|
||||
|
||||
// builds a connection between a service and an exit span
|
||||
connections.push([serviceNode, exitSpanNode]);
|
||||
}
|
||||
|
||||
return {
|
||||
connections,
|
||||
exitSpanDestinations,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue