mirror of
https://github.com/yldio/copilot.git
synced 2024-11-28 06:00:06 +02:00
feat: initial metrics implementation
This commit is contained in:
parent
0d8a282248
commit
1eac90c79a
@ -21,6 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"apollo": "^0.2.2",
|
"apollo": "^0.2.2",
|
||||||
"apr-intercept": "^1.0.4",
|
"apr-intercept": "^1.0.4",
|
||||||
|
"chart.js": "^2.6.0",
|
||||||
"constant-case": "^2.0.0",
|
"constant-case": "^2.0.0",
|
||||||
"force-array": "^3.1.0",
|
"force-array": "^3.1.0",
|
||||||
"graphql-tag": "^2.4.0",
|
"graphql-tag": "^2.4.0",
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export { default as ServiceScale } from './scale';
|
export { default as ServiceScale } from './scale';
|
||||||
export { default as ServiceDelete } from './delete';
|
export { default as ServiceDelete } from './delete';
|
||||||
|
export { default as ServiceMetrics} from './metrics';
|
||||||
|
35
packages/cp-frontend/src/components/service/metrics.js
Normal file
35
packages/cp-frontend/src/components/service/metrics.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { MetricGraph } from 'joyent-ui-toolkit';
|
||||||
|
|
||||||
|
const ServiceMetrics = ({
|
||||||
|
metricsData,
|
||||||
|
graphDurationSeconds
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// metricsData should prob be an array rather than an object
|
||||||
|
const metricGraphs = Object.keys(metricsData).map((key) => (
|
||||||
|
// should also have a header, w metric name and number of instances (omit everything else from design for copilot)
|
||||||
|
<MetricGraph
|
||||||
|
key={key}
|
||||||
|
metricsData={metricsData[key]}
|
||||||
|
width={954}
|
||||||
|
height={292}
|
||||||
|
graphDurationSeconds={graphDurationSeconds}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
// This needs layout!!!
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{metricGraphs}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceMetrics.propTypes = {
|
||||||
|
// metricsData should prob be an array rather than an object
|
||||||
|
metricsData: PropTypes.object.isRequired,
|
||||||
|
graphDurationSeconds: PropTypes.number.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServiceMetrics;
|
1
packages/cp-frontend/src/containers/metrics/index.js
Normal file
1
packages/cp-frontend/src/containers/metrics/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './metrics-data-hoc';
|
128
packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js
Normal file
128
packages/cp-frontend/src/containers/metrics/metrics-data-hoc.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { compose, graphql } from 'react-apollo';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const MetricNames = [
|
||||||
|
'AVG_MEM_BYTES',
|
||||||
|
'AVG_LOAD_PERCENT',
|
||||||
|
'AGG_NETWORK_BYTES'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const withServiceMetricsPolling = ({
|
||||||
|
pollingInterval = 1000 // in milliseconds
|
||||||
|
}) => {
|
||||||
|
return (WrappedComponent) => {
|
||||||
|
|
||||||
|
return class extends Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
|
||||||
|
this._poll = setInterval(() => {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
service,
|
||||||
|
fetchMoreMetrics
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if(!loading && !error && service) {
|
||||||
|
const previousEnd = service.instances[0].metrics[0].end;
|
||||||
|
fetchMoreMetrics(previousEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, pollingInterval); // TODO this is the polling interval - think about amount is the todo I guess...
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this._poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <WrappedComponent {...this.props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withServiceMetricsGql = ({
|
||||||
|
gqlQuery,
|
||||||
|
graphDurationSeconds,
|
||||||
|
updateIntervalSeconds
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const getPreviousMetrics = (previousResult, serviceId, instanceId, metricName) => {
|
||||||
|
return previousResult.deploymentGroup.services
|
||||||
|
.find(s => s.id === serviceId).instances
|
||||||
|
.find(i => i.id === instanceId).metrics
|
||||||
|
.find(m => m.name === metricName).metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNextResult = (previousResult, fetchNextResult) => {
|
||||||
|
const deploymentGroup = fetchNextResult.deploymentGroup;
|
||||||
|
const nextResult = {
|
||||||
|
deploymentGroup: {
|
||||||
|
...deploymentGroup,
|
||||||
|
services: deploymentGroup.services.map(service => ({
|
||||||
|
...service,
|
||||||
|
instances: service.instances.map(instance => ({
|
||||||
|
...instance,
|
||||||
|
metrics: instance.metrics.map(metric => ({
|
||||||
|
...metric,
|
||||||
|
metrics: getPreviousMetrics(
|
||||||
|
previousResult,
|
||||||
|
service.id,
|
||||||
|
instance.id,
|
||||||
|
metric.name
|
||||||
|
).concat(metric.metrics)
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return graphql(gqlQuery, {
|
||||||
|
options(props) {
|
||||||
|
const params = props.match.params;
|
||||||
|
const deploymentGroupSlug = params.deploymentGroup;
|
||||||
|
const serviceSlug = params.service;
|
||||||
|
|
||||||
|
// this is potentially prone to overfetching if we already have data within timeframe and we leave the page then come back to it
|
||||||
|
const end = moment();
|
||||||
|
const start = moment(end).subtract(graphDurationSeconds + updateIntervalSeconds, 'seconds'); // TODO initial amount of data we wanna get - should be the same as what we display + 15 secs
|
||||||
|
|
||||||
|
return {
|
||||||
|
variables: {
|
||||||
|
deploymentGroupSlug,
|
||||||
|
serviceSlug,
|
||||||
|
metricNames: MetricNames,
|
||||||
|
start: start.utc().format(),
|
||||||
|
end: end.utc().format()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: ({ data: { deploymentGroup, loading, error, variables, fetchMore }}) => {
|
||||||
|
|
||||||
|
const fetchMoreMetrics = (previousEnd) => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
...variables,
|
||||||
|
start: previousEnd,
|
||||||
|
end: moment().utc().format()
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult, queryVariables }) => {
|
||||||
|
return getNextResult(previousResult, fetchMoreResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ({
|
||||||
|
deploymentGroup,
|
||||||
|
service: !loading && deploymentGroup ? deploymentGroup.services[0] : null,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchMoreMetrics
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
export { default as ServiceScale } from './scale';
|
export { default as ServiceScale } from './scale';
|
||||||
export { default as ServiceDelete } from './delete';
|
export { default as ServiceDelete } from './delete';
|
||||||
|
export { default as ServiceMetrics } from './metrics';
|
||||||
|
239
packages/cp-frontend/src/containers/service/metrics.js
Normal file
239
packages/cp-frontend/src/containers/service/metrics.js
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { compose, graphql } from 'react-apollo';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import moment from 'moment';
|
||||||
|
import ServiceMetricsQuery from '@graphql/ServiceMetrics.gql';
|
||||||
|
import { withNotFound, GqlPaths } from '@containers/navigation';
|
||||||
|
import { LayoutContainer } from '@components/layout';
|
||||||
|
import { ServiceMetrics as ServiceMetricsComponent } from '@components/service';
|
||||||
|
import { Button } from 'joyent-ui-toolkit';
|
||||||
|
import { Loader, ErrorMessage } from '@components/messaging';
|
||||||
|
|
||||||
|
import { withServiceMetricsPolling, withServiceMetricsGql } from '@containers/metrics';
|
||||||
|
|
||||||
|
// 'width' of graph, i.e. total duration of time it'll display and truncate data to
|
||||||
|
// amount of data we'll need to initially fetch
|
||||||
|
const GraphDurationSeconds = 90;
|
||||||
|
|
||||||
|
const ServiceMetrics = ({
|
||||||
|
service,
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
}) => {
|
||||||
|
if (loading || !service) {
|
||||||
|
return (
|
||||||
|
<LayoutContainer center>
|
||||||
|
<Loader />
|
||||||
|
</LayoutContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<LayoutContainer>
|
||||||
|
<ErrorMessage
|
||||||
|
title="Ooops!"
|
||||||
|
message="An error occurred while loading your metrics."
|
||||||
|
/>
|
||||||
|
</LayoutContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// metricsData should prob be an array rather than an object
|
||||||
|
const metricsData = service.instances.reduce((metrics, instance) => {
|
||||||
|
// gather metrics of instances according to type
|
||||||
|
instance.metrics.forEach((instanceMetrics) => {
|
||||||
|
if(!metrics[instanceMetrics.name]) {
|
||||||
|
metrics[instanceMetrics.name] = [];
|
||||||
|
}
|
||||||
|
metrics[instanceMetrics.name].push(instanceMetrics);
|
||||||
|
});
|
||||||
|
return metrics;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutContainer>
|
||||||
|
<ServiceMetricsComponent
|
||||||
|
metricsData={metricsData}
|
||||||
|
graphDurationSeconds={GraphDurationSeconds}
|
||||||
|
/>
|
||||||
|
</LayoutContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withServiceMetricsGql({
|
||||||
|
gqlQuery: ServiceMetricsQuery,
|
||||||
|
graphDurationSeconds: GraphDurationSeconds,
|
||||||
|
updateIntervalSeconds: 15
|
||||||
|
}),
|
||||||
|
withServiceMetricsPolling({ pollingInterval: 1000 }),
|
||||||
|
withNotFound([
|
||||||
|
GqlPaths.DEPLOYMENT_GROUP,
|
||||||
|
GqlPaths.SERVICES
|
||||||
|
])
|
||||||
|
)(ServiceMetrics);
|
||||||
|
|
||||||
|
/*
|
||||||
|
const metricNames = [
|
||||||
|
'AVG_MEM_BYTES',
|
||||||
|
'AVG_LOAD_PERCENT',
|
||||||
|
'AGG_NETWORK_BYTES'
|
||||||
|
];
|
||||||
|
|
||||||
|
class ServiceMetrics extends Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
|
||||||
|
this._poll = setInterval(() => {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
deploymentGroup,
|
||||||
|
service,
|
||||||
|
fetchMoreMetrics
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if(!loading && service) {
|
||||||
|
const previousEnd = service.instances[0].metrics[0].end;
|
||||||
|
fetchMoreMetrics(previousEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 1000); // TODO this is the polling interval - think about amount is the todo I guess...
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this._poll);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
|
||||||
|
const {
|
||||||
|
service,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchMoreMetrics
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (loading || !service) {
|
||||||
|
return (
|
||||||
|
<LayoutContainer center>
|
||||||
|
<Loader />
|
||||||
|
</LayoutContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<LayoutContainer>
|
||||||
|
<ErrorMessage
|
||||||
|
title="Ooops!"
|
||||||
|
message="An error occurred while loading your metrics."
|
||||||
|
/>
|
||||||
|
</LayoutContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// metricsData should prob be an array rather than an object
|
||||||
|
const metricsData = service.instances.reduce((metrics, instance) => {
|
||||||
|
// gather metrics of instances according to type
|
||||||
|
instance.metrics.forEach((instanceMetrics) => {
|
||||||
|
if(!metrics[instanceMetrics.name]) {
|
||||||
|
metrics[instanceMetrics.name] = [];
|
||||||
|
}
|
||||||
|
metrics[instanceMetrics.name].push(instanceMetrics);
|
||||||
|
});
|
||||||
|
return metrics;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutContainer>
|
||||||
|
<ServiceMetricsComponent metricsData={metricsData} />
|
||||||
|
</LayoutContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPreviousMetrics = (previousResult, serviceId, instanceId, metricName) => {
|
||||||
|
return previousResult.deploymentGroup.services
|
||||||
|
.find(s => s.id === serviceId).instances
|
||||||
|
.find(i => i.id === instanceId).metrics
|
||||||
|
.find(m => m.name === metricName).metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNextResult = (previousResult, fetchNextResult) => {
|
||||||
|
const deploymentGroup = fetchNextResult.deploymentGroup;
|
||||||
|
const nextResult = {
|
||||||
|
deploymentGroup: {
|
||||||
|
...deploymentGroup,
|
||||||
|
services: deploymentGroup.services.map(service => ({
|
||||||
|
...service,
|
||||||
|
instances: service.instances.map(instance => ({
|
||||||
|
...instance,
|
||||||
|
metrics: instance.metrics.map(metric => ({
|
||||||
|
...metric,
|
||||||
|
metrics: getPreviousMetrics(
|
||||||
|
previousResult,
|
||||||
|
service.id,
|
||||||
|
instance.id,
|
||||||
|
metric.name
|
||||||
|
).concat(metric.metrics)
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServiceMetricsGql = graphql(ServiceMetricsQuery, {
|
||||||
|
options(props) {
|
||||||
|
const params = props.match.params;
|
||||||
|
const deploymentGroupSlug = params.deploymentGroup;
|
||||||
|
const serviceSlug = params.service;
|
||||||
|
|
||||||
|
// this is potentially prone to overfetching if we already have data within timeframe and we leave the page then come back to it
|
||||||
|
const end = moment();
|
||||||
|
const start = moment(end).subtract(105, 'seconds'); // TODO initial amount of data we wanna get - should be the same as what we display + 15 secs
|
||||||
|
|
||||||
|
return {
|
||||||
|
variables: {
|
||||||
|
deploymentGroupSlug,
|
||||||
|
serviceSlug,
|
||||||
|
metricNames,
|
||||||
|
start: start.utc().format(),
|
||||||
|
end: end.utc().format()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: ({ data: { deploymentGroup, loading, error, variables, fetchMore }}) => {
|
||||||
|
|
||||||
|
const fetchMoreMetrics = (previousEnd) => {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
...variables,
|
||||||
|
start: previousEnd,
|
||||||
|
end: moment().utc().format()
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult, queryVariables }) => {
|
||||||
|
return getNextResult(previousResult, fetchMoreResult);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ({
|
||||||
|
deploymentGroup,
|
||||||
|
service: !loading && deploymentGroup ? deploymentGroup.services[0] : null,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
fetchMoreMetrics
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
ServiceMetricsGql,
|
||||||
|
withNotFound([
|
||||||
|
GqlPaths.DEPLOYMENT_GROUP,
|
||||||
|
GqlPaths.SERVICES
|
||||||
|
])
|
||||||
|
)(ServiceMetrics); */
|
32
packages/cp-frontend/src/graphql/ServiceMetrics.gql
Normal file
32
packages/cp-frontend/src/graphql/ServiceMetrics.gql
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
query Instances(
|
||||||
|
$deploymentGroupSlug: String!,
|
||||||
|
$serviceSlug: String!,
|
||||||
|
$metricNames: [MetricName]!,
|
||||||
|
$start: String!,
|
||||||
|
$end: String!
|
||||||
|
) {
|
||||||
|
deploymentGroup(slug: $deploymentGroupSlug) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
services(slug: $serviceSlug) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
instances {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
metrics(names: $metricNames, start: $start, end: $end) {
|
||||||
|
instance
|
||||||
|
name
|
||||||
|
start
|
||||||
|
end
|
||||||
|
metrics {
|
||||||
|
time
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { Header, Breadcrumb, Menu } from '@containers/navigation';
|
import { Header, Breadcrumb, Menu } from '@containers/navigation';
|
||||||
import { ServiceScale, ServiceDelete } from '@containers/service';
|
|
||||||
import Manifest from '@containers/manifest';
|
import Manifest from '@containers/manifest';
|
||||||
import Environment from '@containers/environment';
|
import Environment from '@containers/environment';
|
||||||
|
|
||||||
@ -20,6 +19,12 @@ import {
|
|||||||
ServicesQuickActions
|
ServicesQuickActions
|
||||||
} from '@containers/services';
|
} from '@containers/services';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ServiceScale,
|
||||||
|
ServiceDelete,
|
||||||
|
ServiceMetrics
|
||||||
|
} from '@containers/service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InstanceList,
|
InstanceList,
|
||||||
InstancesTooltip
|
InstancesTooltip
|
||||||
@ -167,6 +172,12 @@ const App = p =>
|
|||||||
component={InstanceList}
|
component={InstanceList}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/deployment-groups/:deploymentGroup/services/:service/metrics"
|
||||||
|
exact
|
||||||
|
component={ServiceMetrics}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/deployment-groups/:deploymentGroup/services/:service"
|
path="/deployment-groups/:deploymentGroup/services/:service"
|
||||||
component={serviceRedirect}
|
component={serviceRedirect}
|
||||||
|
@ -23,6 +23,10 @@ const state = {
|
|||||||
{
|
{
|
||||||
pathname: 'instances',
|
pathname: 'instances',
|
||||||
name: 'Instances'
|
name: 'Instances'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pathname: 'metrics',
|
||||||
|
name: 'Metrics'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -28,7 +28,10 @@ export const client = new ApolloClient({
|
|||||||
? o.uuid
|
? o.uuid
|
||||||
: o.timestamp
|
: o.timestamp
|
||||||
? o.timestamp
|
? o.timestamp
|
||||||
: o.name ? o.name : 'apollo-cache-key-not-defined';
|
: o.name && o.instance
|
||||||
|
? `${o.name}-${o.instance}`
|
||||||
|
: o.name ? o.name : o.time && o.value
|
||||||
|
? `${o.time}-${o.value}` : 'apollo-cache-key-not-defined';
|
||||||
return `${o.__typename}:${id}`;
|
return `${o.__typename}:${id}`;
|
||||||
},
|
},
|
||||||
networkInterface: createNetworkInterface({
|
networkInterface: createNetworkInterface({
|
||||||
|
34469
packages/cp-gql-mock-server/src/metric-datasets-0.json
Normal file
34469
packages/cp-gql-mock-server/src/metric-datasets-0.json
Normal file
File diff suppressed because it is too large
Load Diff
34493
packages/cp-gql-mock-server/src/metric-datasets-1.json
Normal file
34493
packages/cp-gql-mock-server/src/metric-datasets-1.json
Normal file
File diff suppressed because it is too large
Load Diff
22996
packages/cp-gql-mock-server/src/metric-datasets-2.json
Normal file
22996
packages/cp-gql-mock-server/src/metric-datasets-2.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,11 +11,16 @@ const uniq = require('lodash.uniq');
|
|||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const hasha = require('hasha');
|
const hasha = require('hasha');
|
||||||
const Boom = require('boom');
|
const Boom = require('boom');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
const wpData = require('./wp-data.json');
|
const wpData = require('./wp-data.json');
|
||||||
const cpData = require('./cp-data.json');
|
const cpData = require('./cp-data.json');
|
||||||
const complexData = require('./complex-data.json');
|
const complexData = require('./complex-data.json');
|
||||||
const metricData = require('./metric-data.json');
|
const metricData = [
|
||||||
|
require('./metric-datasets-0.json'),
|
||||||
|
require('./metric-datasets-1.json'),
|
||||||
|
require('./metric-datasets-2.json')
|
||||||
|
];
|
||||||
|
|
||||||
const { datacenter, portal } = require('./data.json');
|
const { datacenter, portal } = require('./data.json');
|
||||||
|
|
||||||
@ -40,16 +45,55 @@ const find = (query = {}) => item =>
|
|||||||
|
|
||||||
const cleanQuery = (q = {}) => JSON.parse(JSON.stringify(q));
|
const cleanQuery = (q = {}) => JSON.parse(JSON.stringify(q));
|
||||||
|
|
||||||
|
let metricDataIndex = 0;
|
||||||
|
|
||||||
const getMetrics = query => {
|
const getMetrics = query => {
|
||||||
const {
|
const {
|
||||||
names,
|
names,
|
||||||
start,
|
start,
|
||||||
end
|
end,
|
||||||
|
instanceId
|
||||||
} = query;
|
} = query;
|
||||||
|
|
||||||
const metrics = names.reduce((metrics, name) =>
|
const metrics = names.reduce((metrics, name) => {
|
||||||
metrics.concat(metricData.filter(md =>
|
|
||||||
md.name === name)), []);
|
// pick one of the three metric data jsons, so there's variety
|
||||||
|
const index = metricDataIndex%metricData.length;
|
||||||
|
metricDataIndex++;
|
||||||
|
|
||||||
|
const md = metricData[index].find(md => md.name === name);
|
||||||
|
const m = md.metrics;
|
||||||
|
|
||||||
|
const s = moment.utc(start);
|
||||||
|
const e = moment.utc(end);
|
||||||
|
|
||||||
|
// how many records do we need?
|
||||||
|
const duration = e.diff(s); // duration for which we need data
|
||||||
|
const records = Math.floor(duration/15000); // new metric record every 15 secs
|
||||||
|
|
||||||
|
const requiredMetrics = [];
|
||||||
|
let i = 0;
|
||||||
|
const time = moment(s);
|
||||||
|
// start at a random point within the dataset for variety
|
||||||
|
const randomIndex = Math.round(Math.random() * m.length);
|
||||||
|
while(i < records) {
|
||||||
|
const index = (randomIndex + i)%m.length; // loop if not enough data
|
||||||
|
const requiredMetric = m[index];
|
||||||
|
requiredMetric.time = time.add(15, 'seconds').utc().format(); // we should have a new record every 15 secs
|
||||||
|
requiredMetrics.push(requiredMetric);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredMetricData = {
|
||||||
|
instance: instanceId,
|
||||||
|
name,
|
||||||
|
start: s.utc().format(),
|
||||||
|
end: time.utc().format(), // this will be used by the frontend for the next fetch
|
||||||
|
metrics: requiredMetrics
|
||||||
|
}
|
||||||
|
metrics.push(requiredMetricData);
|
||||||
|
return metrics;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return Promise.resolve(metrics);
|
return Promise.resolve(metrics);
|
||||||
}
|
}
|
||||||
|
@ -181,6 +181,8 @@ enum MetricName {
|
|||||||
type InstanceMetric {
|
type InstanceMetric {
|
||||||
instance: String!
|
instance: String!
|
||||||
name: MetricName!
|
name: MetricName!
|
||||||
|
start: String!
|
||||||
|
end: String!
|
||||||
metrics: [Metric]
|
metrics: [Metric]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,3 +107,7 @@ export {
|
|||||||
UnhealthyIcon,
|
UnhealthyIcon,
|
||||||
BinIcon
|
BinIcon
|
||||||
} from './icons';
|
} from './icons';
|
||||||
|
|
||||||
|
export {
|
||||||
|
MetricGraph
|
||||||
|
} from './metrics';
|
||||||
|
153
packages/ui-toolkit/src/metrics/graph.js
Normal file
153
packages/ui-toolkit/src/metrics/graph.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Chart from 'chart.js';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
// colours to come from theme
|
||||||
|
// AJ to supply friendly colours - these were nicked from chartjs utils
|
||||||
|
const chartColors = [
|
||||||
|
'rgb(255, 99, 132)',
|
||||||
|
'rgb(255, 159, 64)',
|
||||||
|
'rgb(255, 205, 86)',
|
||||||
|
'rgb(75, 192, 192)',
|
||||||
|
'rgb(54, 162, 235)',
|
||||||
|
'rgb(153, 102, 255)',
|
||||||
|
'rgb(201, 203, 207)'
|
||||||
|
];
|
||||||
|
|
||||||
|
// TODO DISPLAY TIMES SHOULD NOT BE UTC
|
||||||
|
class MetricGraph extends Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
xMin,
|
||||||
|
xMax,
|
||||||
|
datasets
|
||||||
|
} = this.processProps(this.props);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
type: 'line',
|
||||||
|
data: { datasets },
|
||||||
|
options: {
|
||||||
|
responsive: false, // this needs to be played with
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
display: false // this config doesn't seem to work???
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
display: true, // config for mini should be false
|
||||||
|
type: 'time',
|
||||||
|
distribution: 'linear',
|
||||||
|
time: {
|
||||||
|
unit: 'minute', // this also needs to be played with
|
||||||
|
min: xMin,
|
||||||
|
max: xMax
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
display: true // needs min / max and measurement
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = this._refs.chart.getContext('2d');
|
||||||
|
this._chart = new Chart(ctx, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
processProps(props) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
metricsData,
|
||||||
|
graphDurationSeconds
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const xMax = metricsData[0].end;
|
||||||
|
const xMin = moment.utc(xMax).subtract(graphDurationSeconds, 'seconds').utc().format();
|
||||||
|
|
||||||
|
const datasets = metricsData.map((data, i) => ({
|
||||||
|
fill: false,
|
||||||
|
borderColor: chartColors[i],
|
||||||
|
data: this.truncateAndConvertMetrics(data.metrics, xMin, xMax)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
xMax,
|
||||||
|
xMin,
|
||||||
|
datasets
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
truncateAndConvertMetrics(metrics, xMin, xMax) {
|
||||||
|
|
||||||
|
const xMinMoment = moment.utc(xMin);
|
||||||
|
|
||||||
|
return metrics.reduce((metrics, metric) => {
|
||||||
|
const diff = moment.utc(metric.time).diff(xMinMoment);
|
||||||
|
if(diff > -10000) { // diff comparison is less than zero - otherwise no data for beginning of chart - bug or charjs weirdness?
|
||||||
|
metrics.push({
|
||||||
|
x: metric.time,
|
||||||
|
y: metric.value // value should be converted here to a readable format
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return metrics;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
xMin,
|
||||||
|
xMax,
|
||||||
|
datasets
|
||||||
|
} = this.processProps(nextProps);
|
||||||
|
|
||||||
|
this._chart.data.datasets = datasets;
|
||||||
|
// these need to be set, but don't seem to truncate the data that's displayed
|
||||||
|
this._chart.options.scales.xAxes[0].time.max = xMax;
|
||||||
|
this._chart.options.scales.xAxes[0].time.min = xMin;
|
||||||
|
this._chart.update(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// should not rerender ever, we update only the canvas via chartjs
|
||||||
|
shouldComponentUpdate() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref(name) {
|
||||||
|
this._refs = this._refs || {};
|
||||||
|
|
||||||
|
return el => {
|
||||||
|
this._refs[name] = el;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={this.ref('chart')}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricGraph.propTypes = {
|
||||||
|
metricsData: PropTypes.object.isRequired,
|
||||||
|
width: PropTypes.number.isRequired,
|
||||||
|
height: PropTypes.number.isRequired,
|
||||||
|
graphDurationSeconds: PropTypes.number.isRequired // 'width' of graph, i.e. total duration of time it'll display and truncate data to
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetricGraph;
|
1
packages/ui-toolkit/src/metrics/index.js
Normal file
1
packages/ui-toolkit/src/metrics/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as MetricGraph } from './graph';
|
Loading…
Reference in New Issue
Block a user