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