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