You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2019/02/16 12:22:13 UTC
[incubator-skywalking-ui] branch master updated: Feature: add
database traceList (#230)
This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-skywalking-ui.git
The following commit(s) were added to refs/heads/master by this push:
new d480b7e Feature: add database traceList (#230)
d480b7e is described below
commit d480b7e05517f3f32c172cea52b88ddbad9085fa
Author: Allen Wang <Al...@outlook.com>
AuthorDate: Sat Feb 16 20:22:09 2019 +0800
Feature: add database traceList (#230)
* Feature: add database traceList
* revert submodule commitId.
---
.roadhogrc.mock.js | 3 +-
mock/database.js | 14 +-
src/components/Trace/TraceListDB/index.js | 88 +++++++++
.../components/Trace/TraceListDB/index.less | 45 +++--
src/models/database.js | 113 ++++++++++--
src/routes/Database/Database.js | 196 +++++++++++++++------
6 files changed, 381 insertions(+), 78 deletions(-)
diff --git a/.roadhogrc.mock.js b/.roadhogrc.mock.js
index f5c8d91..1de7bd0 100644
--- a/.roadhogrc.mock.js
+++ b/.roadhogrc.mock.js
@@ -3,7 +3,7 @@ import { delay } from 'roadhog-api-doc';
import { getGlobalTopology, getServiceTopology, getEndpointTopology } from './mock/topology';
import { Alarms, AlarmTrend } from './mock/alarm';
import { TraceBrief, Trace } from './mock/trace'
-import { getAllDatabases } from './mock/database'
+import { getAllDatabases, getTopNRecords } from './mock/database'
import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools';
import { graphql } from 'graphql';
import { ClusterBrief, getServiceInstances, getAllServices, searchEndpoint, EndpointInfo } from './mock/metadata';
@@ -16,6 +16,7 @@ const resolvers = {
Query: {
getAllServices,
getAllDatabases,
+ getTopNRecords,
getServiceInstances,
getServiceTopN,
getAllEndpointTopN,
diff --git a/mock/database.js b/mock/database.js
index dd0f550..0ab977c 100644
--- a/mock/database.js
+++ b/mock/database.js
@@ -17,11 +17,17 @@
import mockjs from 'mockjs';
-export default {
- getAllDatabases: () => {
+ export const getAllDatabases = () => {
const data = mockjs.mock({
'databaseId|20-50': [{ 'id|+1': 3, name: function() { return `database-${this.id}`; }, type: function() { return `type-${this.id}`; } }], // eslint-disable-line
});
return data.databaseId;
- },
-};
+ };
+
+ export const getTopNRecords = () => {
+ const data = mockjs.mock({
+ 'getTopNRecords|20-50': [
+ { 'traceId|+1': '@natural(200, 300).@natural(200, 300).@natural(200, 300).@natural(200, 300)', statement: function() { return `select * from database where complex = @natural(200, 300)`; }, latency: '@natural(200, 300)' }], // eslint-disable-line
+ });
+ return data.getTopNRecords;
+ };
diff --git a/src/components/Trace/TraceListDB/index.js b/src/components/Trace/TraceListDB/index.js
new file mode 100644
index 0000000..62ee941
--- /dev/null
+++ b/src/components/Trace/TraceListDB/index.js
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { PureComponent } from 'react';
+import {List, Button } from 'antd';
+import Ellipsis from 'ant-design-pro/lib/Ellipsis';
+import styles from './index.less';
+
+class TraceList extends PureComponent {
+ renderEndpointName = (opName, duration, maxDuration) => {
+ return (
+ <div className={styles.progressWrap}>
+ <div
+ className={styles.progress}
+ style={{
+ backgroundColor: '#87CEFA',
+ width: `${(duration * 100) / maxDuration}%`,
+ height: 25,
+ }}
+ />
+ <div className={styles.mainInfo}>
+ <Ellipsis length={100} tooltip style={{ width: 'initial' }}>
+ {opName}
+ </Ellipsis>
+ <span className={styles.duration}>{`${duration}ms`}</span>
+ </div>
+ </div>
+ );
+ };
+
+ renderDescription = (start, traceIds) => {
+ const { onClickTraceTag } = this.props;
+ return (
+ <div>
+ <Button size="small" onClick={() => onClickTraceTag(traceIds)}>
+ {traceIds}
+ </Button>
+ </div>
+ );
+ };
+
+ render() {
+ const { data: traces, loading } = this.props;
+ let maxDuration = 0;
+ traces.forEach(item => {
+ if (item.latency > maxDuration) {
+ maxDuration = item.latency;
+ }
+ });
+ return (
+ <List
+ className={styles.traceList}
+ itemLayout="horizontal"
+ size="small"
+ dataSource={traces}
+ loading={loading}
+ renderItem={item => (
+ <List.Item>
+ <List.Item.Meta
+ title={this.renderEndpointName(
+ item.statement,
+ item.latency,
+ maxDuration
+ )}
+ description={this.renderDescription(item.start, item.traceId)}
+ />
+ </List.Item>
+ )}
+ />
+ );
+ }
+}
+
+export default TraceList;
diff --git a/mock/database.js b/src/components/Trace/TraceListDB/index.less
similarity index 55%
copy from mock/database.js
copy to src/components/Trace/TraceListDB/index.less
index dd0f550..2413b19 100644
--- a/mock/database.js
+++ b/src/components/Trace/TraceListDB/index.less
@@ -1,4 +1,4 @@
-/**
+/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
@@ -15,13 +15,38 @@
* limitations under the License.
*/
-import mockjs from 'mockjs';
+@import '~antd/lib/style/themes/default.less';
-export default {
- getAllDatabases: () => {
- const data = mockjs.mock({
- 'databaseId|20-50': [{ 'id|+1': 3, name: function() { return `database-${this.id}`; }, type: function() { return `type-${this.id}`; } }], // eslint-disable-line
- });
- return data.databaseId;
- },
-};
+.traceList {
+ padding: 5px 0;
+ position: relative;
+ width: 100%;
+ .progressWrap {
+ background-color: @background-color-base;
+ position: relative;
+ height: 25px;
+ }
+ .progress {
+ position: absolute;
+ z-index: 10;
+ transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
+ border-radius: 1px 0 0 1px;
+ background-color: @primary-color;
+ width: 0;
+ height: 100%;
+ }
+ .mainInfo {
+ position: absolute;
+ z-index: 20;
+ width: 100%;
+ .duration {
+ float: right;
+ }
+ }
+ .startTime {
+ float: right;
+ }
+ .content {
+ width: 30%;
+ }
+}
diff --git a/src/models/database.js b/src/models/database.js
index d68c293..df163f1 100644
--- a/src/models/database.js
+++ b/src/models/database.js
@@ -17,7 +17,7 @@
import { base } from '../utils/models';
-// import { exec } from '../services/graphql';
+import { exec } from '../services/graphql';
const optionsQuery = `
query DatabaseOption($duration: Duration!) {
@@ -29,6 +29,53 @@ const optionsQuery = `
}
`;
+const TopNRecordsQuery = `
+ query TopNRecords($condition: TopNRecordsCondition!) {
+ getTopNRecords(condition: $condition) {
+ statement
+ latency
+ traceId
+ }
+ }
+`;
+
+const spanQuery = `query Spans($traceId: ID!) {
+ queryTrace(traceId: $traceId) {
+ spans {
+ traceId
+ segmentId
+ spanId
+ parentSpanId
+ refs {
+ traceId
+ parentSegmentId
+ parentSpanId
+ type
+ }
+ serviceCode
+ startTime
+ endTime
+ endpointName
+ type
+ peer
+ component
+ isError
+ layer
+ tags {
+ key
+ value
+ }
+ logs {
+ time
+ data {
+ key
+ value
+ }
+ }
+ }
+ }
+}`;
+
const dataQuery = `
query Database($databaseId: ID!, $duration: Duration!) {
getResponseTimeTrend: getLinearIntValues(metric: {
@@ -127,24 +174,64 @@ export default base({
getP50: {
values: [],
},
+ getTopNRecords: [],
},
optionsQuery,
dataQuery,
effects: {
- // *fetchServiceInstance({ payload }, { call, put }) {
- // const { variables, serviceInstanceInfo } = payload;
- // const response = yield call(exec, { variables, query: serviceInstanceQuery });
- // if (!response.data) {
- // return;
- // }
- // yield put({
- // type: 'saveServiceInstance',
- // payload: response.data,
- // serviceInstanceInfo,
- // });
- // },
+ *fetchTraces({ payload }, { call, put }) {
+ const { variables } = payload;
+ const response = yield call(exec, { variables, query: TopNRecordsQuery });
+ if (!response.data) {
+ return;
+ }
+ yield put({
+ type: 'saveTraces',
+ payload: response.data,
+ });
+ },
+ *fetchSpans({ payload }, { call, put }) {
+ const response = yield call(exec, { query: spanQuery, variables: payload.variables });
+ yield put({
+ type: 'saveSpans',
+ payload: response,
+ traceId: payload.variables.traceId,
+ });
+ },
},
reducers: {
+ saveSpans(state, { payload, traceId }) {
+ const { data } = state;
+ return {
+ ...state,
+ data: {
+ ...data,
+ queryTrace: payload.data.queryTrace,
+ currentTraceId: traceId,
+ showTimeline: true,
+ },
+ };
+ },
+ saveTraces(state, { payload }) {
+ const { data } = state;
+ return {
+ ...state,
+ data: {
+ ...data,
+ getTopNRecords: payload.getTopNRecords,
+ },
+ };
+ },
+ hideTimeline(state) {
+ const { data } = state;
+ return {
+ ...state,
+ data: {
+ ...data,
+ showTimeline: false,
+ },
+ };
+ },
saveDatabase(preState, { payload }) {
const { data } = preState;
return {
diff --git a/src/routes/Database/Database.js b/src/routes/Database/Database.js
index c05d0cc..8e738ab 100644
--- a/src/routes/Database/Database.js
+++ b/src/routes/Database/Database.js
@@ -17,9 +17,11 @@
import React, { Component } from 'react';
import { connect } from 'dva';
-import { Row, Select, Form } from 'antd';
+import { Row, Select, Form, Col, Button, Icon, Card } from 'antd';
import { Panel } from 'components/Page';
import { DatabaseChartArea, DatabaseChartBar, DatabaseChartLine } from 'components/Database';
+import TraceList from '../../components/Trace/TraceListDB';
+import TraceTimeline from '../Trace/TraceTimeline';
import { avgTS } from '../../utils/utils';
import { axisY, axisMY } from '../../utils/time';
@@ -42,6 +44,7 @@ const { Item: FormItem } = Form;
},
})
export default class Database extends Component {
+
componentDidMount() {
const propsData = this.props;
propsData.dispatch({
@@ -49,7 +52,7 @@ export default class Database extends Component {
payload: { variables: propsData.globalVariables },
});
}
-
+
componentWillUpdate(nextProps) {
const propsData = this.props;
if (nextProps.globalVariables.duration === propsData.globalVariables.duration) {
@@ -72,12 +75,62 @@ export default class Database extends Component {
});
}
+ handleSelectTopN = (selected) => {
+ const {...propsData} = this.props;
+ this.topNum = selected;
+ propsData.dispatch({
+ type: 'database/fetchTraces',
+ payload: { variables: {
+ condition:{
+ serviceId:this.databaseId,
+ metricName:"top_n_database_statement",
+ topN:selected,
+ order:'DES',
+ duration:propsData.globalVariables.duration,
+ },
+ }},
+ });
+ }
+
handleChange = (variables) => {
const {...propsData} = this.props;
propsData.dispatch({
type: 'database/fetchData',
payload: { variables, reducer: 'saveDatabase' },
});
+ this.databaseId = variables.databaseId;
+ this.handleGetTraces(variables.databaseId);
+ }
+
+ handleGoBack = () => {
+ const {...propsData} = this.props;
+ propsData.dispatch({
+ type: 'database/hideTimeline',
+ });
+ }
+
+ handleShowTrace = (traceId) => {
+ const { dispatch } = this.props;
+ dispatch({
+ type: 'database/fetchSpans',
+ payload: { variables: { traceId } },
+ });
+ }
+
+ handleGetTraces = (databaseId) => {
+ const {...propsData} = this.props;
+ propsData.dispatch({
+ type: 'database/fetchTraces',
+ payload: { variables: {
+ condition:{
+ serviceId:databaseId,
+ metricName:"top_n_database_statement",
+ topN:this.topNum || 20,
+ order:'DES',
+ duration:propsData.globalVariables.duration,
+ },
+ }},
+ });
}
render() {
@@ -85,57 +138,100 @@ export default class Database extends Component {
const { duration } = this.props;
const { getFieldDecorator } = propsData.form;
const { variables: { values, options }, data } = propsData.database;
+ const { showTimeline, queryTrace, currentTraceId } = data;
return (
<div>
- <Form layout="inline">
- <FormItem style={{ width: '100%' }}>
- {getFieldDecorator('databaseId')(
- <Select
- showSearch
- style={{ minWidth: 350 }}
- optionFilterProp="children"
- placeholder="Select a database"
- labelInValue
- onSelect={this.handleSelect.bind(this)}
- >
- {options.databaseId && options.databaseId.map(db =>
- db.key ?
- <Option key={db.key} value={db.key}>{db.type}: {db.label}</Option>
- :
- null
- )}
- </Select>
- )}
- </FormItem>
- </Form>
- <Panel
- variables={values}
- globalVariables={propsData.globalVariables}
- onChange={this.handleChange}
- >
- <Row>
- <DatabaseChartArea
- title="Avg Throughput"
- total={`${avgTS(data.getThroughputTrend.values)} cpm`}
- data={axisY(duration, data.getThroughputTrend.values)}
- />
- <DatabaseChartArea
- title="Avg Response Time"
- total={`${avgTS(data.getResponseTimeTrend.values)} ms`}
- data={axisY(duration, data.getResponseTimeTrend.values)}
- />
- <DatabaseChartBar
- title="Avg SLA"
- total={`${(avgTS(data.getSLATrend.values) / 100).toFixed(2)} %`}
- data={axisY(duration, data.getSLATrend.values, ({ x, y }) => ({ x, y: y / 100 }))}
- />
+ {showTimeline ? (
+ <Row type="flex" justify="start">
+ <Col style={{ marginBottom: 24 }}>
+ <Button ghost type="primary" size="small" onClick={() => { this.handleGoBack(); }}>
+ <Icon type="left" />Go back
+ </Button>
+ </Col>
</Row>
- <DatabaseChartLine
- title="Response Time"
- data={axisMY(propsData.duration, [{ title: 'p99', value: data.getP99}, { title: 'p95', value: data.getP95},
- { title: 'p90', value: data.getP90}, { title: 'p75', value: data.getP75}, { title: 'p50', value: data.getP50}])}
- />
- </Panel>
+ ) : null}
+ <Row type="flex" justify="start">
+ <Col span={showTimeline ? 0 : 24}>
+ <Form layout="inline">
+ <FormItem style={{ width: '100%' }}>
+ {getFieldDecorator('databaseId')(
+ <Select
+ showSearch
+ style={{ minWidth: 350 }}
+ optionFilterProp="children"
+ placeholder="Select a database"
+ labelInValue
+ onSelect={this.handleSelect.bind(this)}
+ >
+ {options.databaseId && options.databaseId.map(db =>
+ db.key ?
+ <Option key={db.key} value={db.key}>{db.type}: {db.label}</Option>
+ :
+ null
+ )}
+ </Select>
+ )}
+ </FormItem>
+ </Form>
+ <Panel
+ variables={values}
+ globalVariables={propsData.globalVariables}
+ onChange={this.handleChange}
+ >
+ <Row>
+ <DatabaseChartArea
+ title="Avg Throughput"
+ total={`${avgTS(data.getThroughputTrend.values)} cpm`}
+ data={axisY(duration, data.getThroughputTrend.values)}
+ />
+ <DatabaseChartArea
+ title="Avg Response Time"
+ total={`${avgTS(data.getResponseTimeTrend.values)} ms`}
+ data={axisY(duration, data.getResponseTimeTrend.values)}
+ />
+ <DatabaseChartBar
+ title="Avg SLA"
+ total={`${(avgTS(data.getSLATrend.values) / 100).toFixed(2)} %`}
+ data={axisY(duration, data.getSLATrend.values, ({ x, y }) => ({ x, y: y / 100 }))}
+ />
+ </Row>
+ <DatabaseChartLine
+ title="Response Time"
+ data={axisMY(propsData.duration, [{ title: 'p99', value: data.getP99}, { title: 'p95', value: data.getP95},
+ { title: 'p90', value: data.getP90}, { title: 'p75', value: data.getP75}, { title: 'p50', value: data.getP50}])}
+ />
+ <Row gutter={8}>
+ <Col xs={24} sm={24} md={24} lg={24} xl={24} style={{ marginTop: 8 }}>
+ <Card>
+ <span>Top </span>
+ <Select
+ style={{ minWidth: 70 }}
+ defaultValue={20}
+ onSelect={this.handleSelectTopN.bind(this)}
+ >
+ <Option value={20}>20</Option>
+ <Option value={50}>50</Option>
+ <Option value={100}>100</Option>
+ </Select>
+ <span> Slow Traces</span>
+ <TraceList
+ data={data.getTopNRecords}
+ onClickTraceTag={this.handleShowTrace}
+ loading={propsData.loading}
+ />
+ </Card>
+ </Col>
+ </Row>
+ </Panel>
+ </Col>
+ <Col span={showTimeline ? 24 : 0}>
+ {showTimeline ? (
+ <TraceTimeline
+ trace={{ data: { queryTrace, currentTraceId } }}
+ />
+ ) : null}
+ </Col>
+ </Row>
</div>
);
}