From b7a8ab5ad6d268e38b5ab57bfaacb8b99ee15e1a Mon Sep 17 00:00:00 2001 From: Tim Raymond Date: Thu, 1 Dec 2016 11:14:31 -0500 Subject: [PATCH 1/3] Add ability to filter by tags on hosts If users have hosts with tags specifying things like fstype=ext3, they want to be able to filter by that. To facilitate this, we have to perform additional parsing of the series that we get back from the initial `SHOW SERIES` that we issue to figure out the apps for hosts. This is parsed out into an object with a shape like: ``` { "measurement" : "foo", "tags" : { "host" : "skeletor", "cpu" : "cpu", "fstype" : "overlay" } } ``` The host is extracted and used for looking up apps as before, however now all tags are also assigned to that host as well. These are then filtered against in SearchBar. Performance is less than ideal with large numbers of hosts, causing page lockup for about 1s each type a character is typed. The `render` function of HostTable.js is approximately 300ms in some profiles that I've taken, which seems very high. Upon further investigation, it seems like `filterHosts` only takes approx 20ms in profiles taken, so the issue appears to be the render path and not related to this patch. --- ui/src/hosts/apis/index.js | 56 ++++++++++++++++++++++++--- ui/src/hosts/components/HostsTable.js | 12 +++++- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/ui/src/hosts/apis/index.js b/ui/src/hosts/apis/index.js index 0fae57bfc..7c38b21b3 100644 --- a/ui/src/hosts/apis/index.js +++ b/ui/src/hosts/apis/index.js @@ -62,12 +62,9 @@ export function getAppsForHosts(proxyLink, hosts, appMappings, telegrafDB) { const newHosts = Object.assign({}, hosts); const allSeries = _.get(resp, ['data', 'results', '0', 'series', '0', 'values'], []); allSeries.forEach(([series]) => { - const matches = series.match(/(\w*).*,host=([^,]*)/); - if (!matches || matches.length !== 3) { // eslint-disable-line no-magic-numbers - return; - } - const measurement = matches[1]; - const host = matches[2]; + const seriesObj = parseSeries(series); + const measurement = seriesObj.measurement; + const host = seriesObj.tags.host; if (!newHosts[host]) { return; @@ -75,7 +72,11 @@ export function getAppsForHosts(proxyLink, hosts, appMappings, telegrafDB) { if (!newHosts[host].apps) { newHosts[host].apps = []; } + if (!newHosts[host].tags) { + newHosts[host].tags = {}; + } newHosts[host].apps = _.uniq(newHosts[host].apps.concat(measurementsToApps[measurement])); + _.assign(newHosts[host].tags, seriesObj.tags); }); return newHosts; @@ -99,6 +100,49 @@ export function getMeasurementsForHost(source, host) { }); } +function parseSeries(series) { + const ident = /\w+/; + const tag = /,?([^=]+)=([^,]+)/; + + function parseMeasurement(s, obj) { + const match = ident.exec(s); + const measurement = match[0]; + if (measurement) { + obj.measurement = measurement; + } + return s.slice(match.index + measurement.length); + } + + function parseTag(s, obj) { + const match = tag.exec(s); + + const kv = match[0]; + const key = match[1]; + const value = match[2]; + + if (key) { + if (!obj.tags) { + obj.tags = {}; + } + obj.tags[key] = value; + } + return s.slice(match.index + kv.length); + } + + let workStr = series.slice(); + const out = {}; + + // Consume measurement + workStr = parseMeasurement(workStr, out); + + // Consume tags + while (workStr.length > 0) { + workStr = parseTag(workStr, out); + } + + return out; +} + function _isEmpty(resp) { return !resp.results[0].series; } diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index a60474fc1..c93cfad89 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -37,9 +37,19 @@ const HostsTable = React.createClass({ } else { apps = ''; } + // search each tag for the presence of the search term + let tagResult = false; + if (h.tags) { + tagResult = Object.keys(h.tags).reduce((acc, key) => { + return acc || h.tags[key].search(searchTerm) !== -1; + }, false); + } else { + tagResult = false; + } return ( h.name.search(searchTerm) !== -1 || - apps.search(searchTerm) !== -1 + apps.search(searchTerm) !== -1 || + tagResult ); }); this.setState({searchTerm, filteredHosts: hosts}); From e06f1c5cf43899d115e6221ac01d10e421bd1683 Mon Sep 17 00:00:00 2001 From: Tim Raymond Date: Thu, 1 Dec 2016 11:35:00 -0500 Subject: [PATCH 2/3] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d54c7726..93443f0c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.1.0 [unreleased] +- #635: Add the ability to filter hosts by any tag associated with that host. - #573: Provide more control over layout rendering when needed with "autolayout" flag on templates. - #586: Allow telegraf database in non-default locations From 9dc8e02b1d8a3baadc6c8679634ad50a055f1254 Mon Sep 17 00:00:00 2001 From: Will Piers Date: Thu, 1 Dec 2016 16:31:21 -0800 Subject: [PATCH 3/3] Refactor HostsTable for fun and SPEED --- ui/src/hosts/components/HostsTable.js | 144 ++++++++++++++++---------- 1 file changed, 89 insertions(+), 55 deletions(-) diff --git a/ui/src/hosts/components/HostsTable.js b/ui/src/hosts/components/HostsTable.js index c93cfad89..c866952b6 100644 --- a/ui/src/hosts/components/HostsTable.js +++ b/ui/src/hosts/components/HostsTable.js @@ -19,24 +19,14 @@ const HostsTable = React.createClass({ getInitialState() { return { searchTerm: '', - filteredHosts: this.props.hosts, sortDirection: null, sortKey: null, }; }, - componentWillReceiveProps(newProps) { - this.filterHosts(newProps.hosts, this.state.searchTerm); - }, - - filterHosts(allHosts, searchTerm) { - const hosts = allHosts.filter((h) => { - let apps = null; - if (h.apps) { - apps = h.apps.join(', '); - } else { - apps = ''; - } + filter(allHosts, searchTerm) { + return allHosts.filter((h) => { + const apps = h.apps ? h.apps.join(', ') : ''; // search each tag for the presence of the search term let tagResult = false; if (h.tags) { @@ -52,17 +42,6 @@ const HostsTable = React.createClass({ tagResult ); }); - this.setState({searchTerm, filteredHosts: hosts}); - }, - - changeSort(key) { - // if we're using the key, reverse order; otherwise, set it with ascending - if (this.state.sortKey === key) { - const reverseDirection = (this.state.sortDirection === 'asc' ? 'desc' : 'asc'); - this.setState({sortDirection: reverseDirection}); - } else { - this.setState({sortKey: key, sortDirection: 'asc'}); - } }, sort(hosts, key, direction) { @@ -76,6 +55,20 @@ const HostsTable = React.createClass({ } }, + updateSearchTerm(term) { + this.setState({searchTerm: term}); + }, + + updateSort(key) { + // if we're using the key, reverse order; otherwise, set it with ascending + if (this.state.sortKey === key) { + const reverseDirection = (this.state.sortDirection === 'asc' ? 'desc' : 'asc'); + this.setState({sortDirection: reverseDirection}); + } else { + this.setState({sortKey: key, sortDirection: 'asc'}); + } + }, + sortableClasses(key) { if (this.state.sortKey === key) { if (this.state.sortDirection === 'asc') { @@ -87,52 +80,32 @@ const HostsTable = React.createClass({ }, render() { - const hosts = this.sort(this.state.filteredHosts, this.state.sortKey, this.state.sortDirection); - const hostCount = hosts.length; - const {source} = this.props; + const {searchTerm, sortKey, sortDirection} = this.state; + const {hosts, source} = this.props; + const sortedHosts = this.sort(this.filter(hosts, searchTerm), sortKey, sortDirection); + const hostCount = sortedHosts.length; return (

{hostCount ? `${hostCount} Hosts` : ''}

- +
- + - - + + { - hosts.map(({name, cpu, load, apps = []}) => { - return ( - - - - - - - - ); + sortedHosts.map((h) => { + return ; }) } @@ -143,13 +116,74 @@ const HostsTable = React.createClass({ }, }); +const HostRow = React.createClass({ + propTypes: { + source: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, + host: PropTypes.shape({ + name: PropTypes.string, + cpu: PropTypes.number, + load: PropTypes.number, + apps: PropTypes.arrayOf(PropTypes.string.isRequired), + }), + }, + + shouldComponentUpdate(nextProps) { + return this.props.host !== nextProps.host; + }, + + render() { + const {host, source} = this.props; + const {name, cpu, load, apps = []} = host; + return ( + + + + + + + + ); + }, +}); + const SearchBar = React.createClass({ propTypes: { onSearch: PropTypes.func.isRequired, }, + getInitialState() { + return { + searchTerm: '', + }; + }, + + componentWillMount() { + const waitPeriod = 300; + this.handleSearch = _.debounce(this.handleSearch, waitPeriod); + }, + + handleSearch() { + this.props.onSearch(this.state.searchTerm); + }, + handleChange() { - this.props.onSearch(this.refs.searchInput.value); + this.setState({searchTerm: this.refs.searchInput.value}, this.handleSearch); }, render() {
this.changeSort('name')} className={this.sortableClasses('name')}>Hostname this.updateSort('name')} className={this.sortableClasses('name')}>Hostname Status this.changeSort('cpu')} className={this.sortableClasses('cpu')}>CPU this.changeSort('load')} className={this.sortableClasses('load')}>Load this.updateSort('cpu')} className={this.sortableClasses('cpu')}>CPU this.updateSort('load')} className={this.sortableClasses('load')}>Load Apps
{name}
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`} - {apps.map((app, index) => { - return ( - - - {app} - - {index === apps.length - 1 ? null : ', '} - - ); - })} -
{name}
{isNaN(cpu) ? 'N/A' : `${cpu.toFixed(2)}%`}{isNaN(load) ? 'N/A' : `${load.toFixed(2)}`} + {apps.map((app, index) => { + return ( + + + {app} + + {index === apps.length - 1 ? null : ', '} + + ); + })} +