import 'whatwg-fetch'; import { emptyMetric, processMultiResourceRollup, processSingleResourceRollup } from './util/MetricUtils.jsx'; import { resourceTypeToCamelCase, singularResource } from './util/Utils.js'; import AddResources from './AddResources.jsx'; import ErrorBanner from './ErrorBanner.jsx'; import Grid from '@material-ui/core/Grid'; import MetricsTable from './MetricsTable.jsx'; import Octopus from './Octopus.jsx'; import PropTypes from 'prop-types'; import React from 'react'; import SimpleChip from './util/Chip.jsx'; import Spinner from './util/Spinner.jsx'; import TopRoutesTabs from './TopRoutesTabs.jsx'; import Typography from '@material-ui/core/Typography'; import _filter from 'lodash/filter'; import _get from 'lodash/get'; import _isEmpty from 'lodash/isEmpty'; import _isEqual from 'lodash/isEqual'; import _merge from 'lodash/merge'; import _reduce from 'lodash/reduce'; import { withContext } from './util/AppContext.jsx'; // if there has been no traffic for some time, show a warning const showNoTrafficMsgDelayMs = 6000; const getResourceFromUrl = (match, pathPrefix) => { let resource = { namespace: match.params.namespace }; let regExp = RegExp(`${pathPrefix || ""}/namespaces/${match.params.namespace}/([^/]+)/([^/]+)`); let urlParts = match.url.match(regExp); resource.type = singularResource(urlParts[1]); resource.name = urlParts[2]; if (match.params[resource.type] !== resource.name) { console.error("Failed to extract resource from URL"); } return resource; }; export class ResourceDetailBase extends React.Component { static propTypes = { api: PropTypes.shape({ PrefixedLink: PropTypes.func.isRequired, }).isRequired, match: PropTypes.shape({ url: PropTypes.string.isRequired }).isRequired, pathPrefix: PropTypes.string.isRequired } constructor(props) { super(props); this.api = this.props.api; this.unmeshedSources = {}; this.handleApiError = this.handleApiError.bind(this); this.loadFromServer = this.loadFromServer.bind(this); this.state = this.getInitialState(props.match, props.pathPrefix); } getInitialState(match, pathPrefix) { let resource = getResourceFromUrl(match, pathPrefix); return { namespace: resource.namespace, resourceName: resource.name, resourceType: resource.type, resource, lastMetricReceivedTime: Date.now(), pollingInterval: 2000, resourceMetrics: [], podMetrics: [], // metrics for all pods whose owner is this resource upstreamMetrics: {}, // metrics for resources who send traffic to this resource downstreamMetrics: {}, // metrics for resources who this resouce sends traffic to unmeshedSources: {}, resourceIsMeshed: true, pendingRequests: false, loaded: false, error: null }; } componentDidMount() { this.loadFromServer(); this.timerId = window.setInterval(this.loadFromServer, this.state.pollingInterval); } componentDidUpdate(prevProps) { if (!_isEqual(prevProps.match.url, this.props.match.url)) { // React won't unmount this component when switching resource pages so we need to clear state this.api.cancelCurrentRequests(); this.unmeshedSources = {}; this.setState(this.getInitialState(this.props.match, this.props.pathPrefix)); } } componentWillUnmount() { window.clearInterval(this.timerId); this.api.cancelCurrentRequests(); } getDisplayMetrics(metricsByResource) { // if we're displaying a pod detail page, only display pod metrics // if we're displaying another type of resource page, display metrics for // rcs, deploys, replicasets, etc but not pods or authorities let shouldExclude = this.state.resourceType === "pod" ? r => r !== "pod" : r => r === "pod" || r === "authority" || r === "service"; return _reduce(metricsByResource, (mem, resourceMetrics, resource) => { if (shouldExclude(resource)) { return mem; } return mem.concat(resourceMetrics); }, []); } loadFromServer() { if (this.state.pendingRequests) { return; // don't make more requests if the ones we sent haven't completed } this.setState({ pendingRequests: true }); let { resource } = this.state; this.api.setCurrentRequests([ // inbound stats for this resource this.api.fetchMetrics( `${this.api.urlsForResource(resource.type, resource.namespace)}&resource_name=${resource.name}` ), // list of all pods in this namespace (hack since we can't currently query for all pods in a resource) this.api.fetchPods(resource.namespace), // metrics for all pods in this namespace (hack, continued) this.api.fetchMetrics( `${this.api.urlsForResource("pod", resource.namespace)}&tcp_stats=true` ), // upstream resources of this resource (meshed traffic only) this.api.fetchMetrics( `${this.api.urlsForResource("all")}&to_name=${resource.name}&to_type=${resource.type}&to_namespace=${resource.namespace}` ), // downstream resources of this resource (meshed traffic only) this.api.fetchMetrics( `${this.api.urlsForResource("all")}&from_name=${resource.name}&from_type=${resource.type}&from_namespace=${resource.namespace}` ) ]); Promise.all(this.api.getCurrentPromises()) .then(([resourceRsp, podListRsp, podMetricsRsp, upstreamRsp, downstreamRsp]) => { let resourceMetrics = processSingleResourceRollup(resourceRsp); let podMetrics = processSingleResourceRollup(podMetricsRsp); let upstreamMetrics = processMultiResourceRollup(upstreamRsp); let downstreamMetrics = processMultiResourceRollup(downstreamRsp); // INEFFICIENT: get metrics for all the pods belonging to this resource. // Do this by querying for metrics for all pods in this namespace and then filtering // out those pods whose owner is not this resource // TODO: fix (#1467) let resourceName = resource.namespace + "/" + resource.name; let podMetricsForResource; if (resource.type === "pod") { // get only info for the pod whose ResourceDetail is being shown // pod.name in podMetrics is of the form `pod-name` podMetricsForResource = _filter(podMetrics, pod => pod.name === resource.name); } else { let podBelongsToResource = _reduce(podListRsp.pods, (mem, pod) => { if (_get(pod, resourceTypeToCamelCase(resource.type)) === resourceName) { // pod.name in podListRsp is of the form `namespace/pod-name` mem[pod.name] = true; } return mem; }, {}); // get all pods whose owner is this resource podMetricsForResource = _filter(podMetrics, pod => podBelongsToResource[pod.namespace + "/" + pod.name]); } let resourceIsMeshed = true; if (!_isEmpty(this.state.resourceMetrics)) { resourceIsMeshed = _get(this.state.resourceMetrics, '[0].pods.meshedPods') > 0; } let lastMetricReceivedTime = this.state.lastMetricReceivedTime; if (!_isEmpty(resourceMetrics[0]) && resourceMetrics[0].totalRequests !== 0) { lastMetricReceivedTime = Date.now(); } this.setState({ resourceMetrics, resourceIsMeshed, podMetrics: podMetricsForResource, upstreamMetrics, downstreamMetrics, lastMetricReceivedTime, loaded: true, pendingRequests: false, error: null, unmeshedSources: this.unmeshedSources // in place of debouncing, just update this when we update the rest of the state }); }) .catch(this.handleApiError); } handleApiError = e => { if (e.isCanceled) { return; } this.setState({ loaded: true, pendingRequests: false, error: e }); } updateUnmeshedSources = obj => { this.unmeshedSources = obj; } banner = () => { if (!this.state.error) { return; } return ; } content = () => { if (!this.state.loaded && !this.state.error) { return ; } let { resourceName, resourceType, namespace, resourceMetrics, unmeshedSources, resourceIsMeshed, lastMetricReceivedTime } = this.state; let query = { resourceName, resourceType, namespace }; let unmeshed = _filter(unmeshedSources, d => d.type !== "pod") .map(d => _merge({}, emptyMetric, d, { unmeshed: true, pods: { totalPods: d.pods.length, meshedPods: 0 } })); let upstreamMetrics = this.getDisplayMetrics(this.state.upstreamMetrics); let downstreamMetrics = this.getDisplayMetrics(this.state.downstreamMetrics); let upstreams = upstreamMetrics.concat(unmeshed); let showNoTrafficMsg = resourceIsMeshed && (Date.now() - lastMetricReceivedTime > showNoTrafficMsgDelayMs); return (
{resourceType}/{resourceName} { showNoTrafficMsg ? : null } { resourceIsMeshed ? : } { resourceIsMeshed ? null : } { _isEmpty(upstreams) ? null : ( Inbound ) } { _isEmpty(this.state.downstreamMetrics) ? null : ( Outbound ) } { this.state.resource.type === "pod" ? null : ( Pods ) } TCP
); } render() { return (
{this.banner()} {this.content()}
); } } export default withContext(ResourceDetailBase);