diff --git a/chronograf.go b/chronograf.go index faf9dd6c5c..adb7ae2ad1 100644 --- a/chronograf.go +++ b/chronograf.go @@ -647,3 +647,9 @@ type BuildStore interface { Get(context.Context) (BuildInfo, error) Update(context.Context, BuildInfo) error } + +// Environement is the set of front-end exposed environment variables +// that were set on the server +type Environment struct { + TelegrafSystemInterval time.Duration `json:"telegrafSystemInterval"` +} diff --git a/server/env.go b/server/env.go new file mode 100644 index 0000000000..9d58fd5c1c --- /dev/null +++ b/server/env.go @@ -0,0 +1,27 @@ +package server + +import ( + "net/http" + + "github.com/influxdata/chronograf" +) + +type envResponse struct { + Links selfLinks `json:"links"` + TelegrafSystemInterval string `json:"telegrafSystemInterval"` +} + +func newEnvResponse(env chronograf.Environment) *envResponse { + return &envResponse{ + Links: selfLinks{ + Self: "/chronograf/v1/env", + }, + TelegrafSystemInterval: env.TelegrafSystemInterval.String(), + } +} + +// Environment retrieves the global application configuration +func (s *Service) Environment(w http.ResponseWriter, r *http.Request) { + res := newEnvResponse(s.Env) + encodeJSON(w, http.StatusOK, res, s.Logger) +} diff --git a/server/env_test.go b/server/env_test.go new file mode 100644 index 0000000000..22e379c5c4 --- /dev/null +++ b/server/env_test.go @@ -0,0 +1,70 @@ +package server + +import ( + "io/ioutil" + "net/http/httptest" + "testing" + "time" + + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/log" +) + +func TestEnvironment(t *testing.T) { + type fields struct { + Environment chronograf.Environment + } + type wants struct { + statusCode int + contentType string + body string + } + + tests := []struct { + name string + fields fields + wants wants + }{ + { + name: "Get environment", + fields: fields{ + Environment: chronograf.Environment{ + TelegrafSystemInterval: 1 * time.Minute, + }, + }, + wants: wants{ + statusCode: 200, + contentType: "application/json", + body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"1m0s"}`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Service{ + Env: tt.fields.Environment, + Logger: log.New(log.DebugLevel), + } + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "http://any.url", nil) + + s.Environment(w, r) + + resp := w.Result() + content := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wants.statusCode { + t.Errorf("%q. Config() = %v, want %v", tt.name, resp.StatusCode, tt.wants.statusCode) + } + if tt.wants.contentType != "" && content != tt.wants.contentType { + t.Errorf("%q. Config() = %v, want %v", tt.name, content, tt.wants.contentType) + } + if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq { + t.Errorf("%q. Config() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body) + } + }) + } +} diff --git a/server/mux.go b/server/mux.go index 9a1639aa5d..aaa1648ea8 100644 --- a/server/mux.go +++ b/server/mux.go @@ -242,6 +242,8 @@ func NewMux(opts MuxOpts, service Service) http.Handler { router.GET("/chronograf/v1/config/:section", EnsureSuperAdmin(service.ConfigSection)) router.PUT("/chronograf/v1/config/:section", EnsureSuperAdmin(service.ReplaceConfigSection)) + router.GET("/chronograf/v1/env", EnsureViewer(service.Environment)) + allRoutes := &AllRoutes{ Logger: opts.Logger, StatusFeed: opts.StatusFeedURL, diff --git a/server/routes.go b/server/routes.go index 497ed21e1d..993d4d7493 100644 --- a/server/routes.go +++ b/server/routes.go @@ -35,6 +35,7 @@ type getRoutesResponse struct { Mappings string `json:"mappings"` // Location of the application mappings endpoint Sources string `json:"sources"` // Location of the sources endpoint Me string `json:"me"` // Location of the me endpoint + Environment string `json:"environment"` // Location of the environement endpoint Dashboards string `json:"dashboards"` // Location of the dashboards endpoint Config getConfigLinksResponse `json:"config"` // Location of the config endpoint and its various sections Auth []AuthRoute `json:"auth"` // Location of all auth routes. @@ -67,6 +68,7 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) { Users: "/chronograf/v1/users", Organizations: "/chronograf/v1/organizations", Me: "/chronograf/v1/me", + Environment: "/chronograf/v1/env", Mappings: "/chronograf/v1/mappings", Dashboards: "/chronograf/v1/dashboards", Config: getConfigLinksResponse{ diff --git a/server/routes_test.go b/server/routes_test.go index bb4203f75f..aeadcd584c 100644 --- a/server/routes_test.go +++ b/server/routes_test.go @@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) { if err := json.Unmarshal(body, &routes); err != nil { t.Error("TestAllRoutes not able to unmarshal JSON response") } - want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}} + want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""}} ` if want != string(body) { t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body)) @@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) { if err := json.Unmarshal(body, &routes); err != nil { t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response") } - want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}} + want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""}} ` if want != string(body) { t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body)) @@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) { if err := json.Unmarshal(body, &routes); err != nil { t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response") } - want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}} + want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]}} ` if want != string(body) { t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body)) diff --git a/server/server.go b/server/server.go index 420cb3c5c8..ee06bada91 100644 --- a/server/server.go +++ b/server/server.go @@ -86,8 +86,9 @@ type Server struct { Auth0ClientSecret string `long:"auth0-client-secret" description:"Auth0 Client Secret for OAuth2 support" env:"AUTH0_CLIENT_SECRET"` Auth0Organizations []string `long:"auth0-organizations" description:"Auth0 organizations permitted to access Chronograf (comma separated)" env:"AUTH0_ORGS" env-delim:","` - StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"` - CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","` + StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"` + CustomLinks map[string]string `long:"custom-link" description:"Custom link to be added to the client User menu. Multiple links can be added by using multiple of the same flag with different 'name:url' values, or as an environment variable with comma-separated 'name:url' values. E.g. via flags: '--custom-link=InfluxData:https://www.influxdata.com --custom-link=Chronograf:https://github.com/influxdata/chronograf'. E.g. via environment variable: 'export CUSTOM_LINKS=InfluxData:https://www.influxdata.com,Chronograf:https://github.com/influxdata/chronograf'" env:"CUSTOM_LINKS" env-delim:","` + TelegrafSystemInterval time.Duration `long:"telegraf-system-interval" default:"1m" description:"Duration used in the GROUP BY time interval for the hosts list" env:"TELEGRAF_SYSTEM_INTERVAL"` ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` @@ -325,6 +326,9 @@ func (s *Server) Serve(ctx context.Context) error { return err } service := openService(ctx, s.BuildInfo, s.BoltPath, s.newBuilders(logger), logger, s.useAuth()) + service.Env = chronograf.Environment{ + TelegrafSystemInterval: s.TelegrafSystemInterval, + } if err := service.HandleNewSources(ctx, s.NewSources); err != nil { logger. WithField("component", "server"). diff --git a/server/service.go b/server/service.go index afe69e6dff..e1df8da8ef 100644 --- a/server/service.go +++ b/server/service.go @@ -15,6 +15,7 @@ type Service struct { TimeSeriesClient TimeSeriesClient Logger chronograf.Logger UseAuth bool + Env chronograf.Environment Databases chronograf.Databases } diff --git a/ui/src/hosts/apis/index.js b/ui/src/hosts/apis/index.js index 584ca4b5a7..413a6cadcd 100644 --- a/ui/src/hosts/apis/index.js +++ b/ui/src/hosts/apis/index.js @@ -2,15 +2,19 @@ import {proxy} from 'utils/queryUrlGenerator' import AJAX from 'utils/ajax' import _ from 'lodash' -export function getCpuAndLoadForHosts(proxyLink, telegrafDB) { +export const getCpuAndLoadForHosts = ( + proxyLink, + telegrafDB, + telegrafSystemInterval +) => { return proxy({ source: proxyLink, query: `SELECT mean("usage_user") FROM cpu WHERE "cpu" = 'cpu-total' AND time > now() - 10m GROUP BY host; SELECT mean("load1") FROM "system" WHERE time > now() - 10m GROUP BY host; - SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM "system" WHERE time > now() - 10m GROUP BY host, time(1m) fill(0); + SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM "system" WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0); SELECT mean("Percent_Processor_Time") FROM win_cpu WHERE time > now() - 10m GROUP BY host; SELECT mean("Processor_Queue_Length") FROM win_system WHERE time > now() - 10s GROUP BY host; - SELECT non_negative_derivative(mean("System_Up_Time")) AS winDeltaUptime FROM win_system WHERE time > now() - 10m GROUP BY host, time(1m) fill(0); + SELECT non_negative_derivative(mean("System_Up_Time")) AS winDeltaUptime FROM win_system WHERE time > now() - ${telegrafSystemInterval} * 10 GROUP BY host, time(${telegrafSystemInterval}) fill(0); SHOW TAG VALUES WITH KEY = "host";`, db: telegrafDB, }).then(resp => { @@ -116,7 +120,7 @@ export const getLayouts = () => resource: 'layouts', }) -export function getAppsForHosts(proxyLink, hosts, appLayouts, telegrafDB) { +export const getAppsForHosts = (proxyLink, hosts, appLayouts, telegrafDB) => { const measurements = appLayouts.map(m => `^${m.measurement}$`).join('|') const measurementsToApps = _.zipObject( appLayouts.map(m => m.measurement), diff --git a/ui/src/hosts/containers/HostsPage.js b/ui/src/hosts/containers/HostsPage.js index b08e324448..a58c446803 100644 --- a/ui/src/hosts/containers/HostsPage.js +++ b/ui/src/hosts/containers/HostsPage.js @@ -1,10 +1,12 @@ import React, {PropTypes, Component} from 'react' +import {connect} from 'react-redux' import _ from 'lodash' import HostsTable from 'src/hosts/components/HostsTable' import SourceIndicator from 'shared/components/SourceIndicator' import {getCpuAndLoadForHosts, getLayouts, getAppsForHosts} from '../apis' +import {getEnv} from 'src/shared/apis/env' class HostsPage extends Component { constructor(props) { @@ -17,48 +19,72 @@ class HostsPage extends Component { } } - componentDidMount() { - const {source, addFlashMessage} = this.props - Promise.all([ - getCpuAndLoadForHosts(source.links.proxy, source.telegraf), - getLayouts(), - new Promise(resolve => { - this.setState({hostsLoading: true}) - resolve() - }), - ]) - .then(([hosts, {data: {layouts}}]) => { - this.setState({ - hosts, - hostsLoading: false, - }) - getAppsForHosts(source.links.proxy, hosts, layouts, source.telegraf) - .then(newHosts => { - this.setState({ - hosts: newHosts, - hostsError: '', - hostsLoading: false, - }) - }) - .catch(error => { - console.error(error) - const reason = 'Unable to get apps for hosts' - addFlashMessage({type: 'error', text: reason}) - this.setState({ - hostsError: reason, - hostsLoading: false, - }) - }) + async componentDidMount() { + const {source, links, addFlashMessage} = this.props + + const {telegrafSystemInterval} = await getEnv(links.environment) + + const hostsError = 'Unable to get apps for hosts' + let hosts, layouts + + try { + const [h, {data}] = await Promise.all([ + getCpuAndLoadForHosts( + source.links.proxy, + source.telegraf, + telegrafSystemInterval + ), + getLayouts(), + new Promise(resolve => { + this.setState({hostsLoading: true}) + resolve() + }), + ]) + + hosts = h + layouts = data.layouts + + this.setState({ + hosts, + hostsLoading: false, }) - .catch(reason => { - this.setState({ - hostsError: reason.toString(), - hostsLoading: false, - }) - // TODO: this isn't reachable at the moment, because getCpuAndLoadForHosts doesn't fail when it should. - // (like with a bogus proxy link). We should provide better messaging to the user in this catch after that's fixed. - console.error(reason) // eslint-disable-line no-console + } catch (error) { + this.setState({ + hostsError: error.toString(), + hostsLoading: false, }) + + console.error(error) + } + + if (!hosts || !layouts) { + addFlashMessage({type: 'error', text: hostsError}) + return this.setState({ + hostsError, + hostsLoading: false, + }) + } + + try { + const newHosts = await getAppsForHosts( + source.links.proxy, + hosts, + layouts, + source.telegraf + ) + this.setState({ + hosts: newHosts, + hostsError: '', + hostsLoading: false, + }) + } catch (error) { + console.error(error) + addFlashMessage({type: 'error', text: hostsError}) + this.setState({ + hostsError, + hostsLoading: false, + }) + } } render() { @@ -97,6 +123,12 @@ class HostsPage extends Component { const {func, shape, string} = PropTypes +const mapStateToProps = ({links}) => { + return { + links, + } +} + HostsPage.propTypes = { source: shape({ id: string.isRequired, @@ -107,7 +139,10 @@ HostsPage.propTypes = { }).isRequired, telegraf: string.isRequired, }), + links: shape({ + environment: string.isRequired, + }), addFlashMessage: func, } -export default HostsPage +export default connect(mapStateToProps, null)(HostsPage) diff --git a/ui/src/shared/actions/auth.js b/ui/src/shared/actions/auth.js index 79aeaa510c..5e8308debd 100644 --- a/ui/src/shared/actions/auth.js +++ b/ui/src/shared/actions/auth.js @@ -61,13 +61,15 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => { const { data: me, auth, - logoutLink, - external, users, - organizations, meLink, config, + external, + logoutLink, + organizations, + environment, } = await getMeAJAX() + dispatch( meGetCompleted({ me, @@ -75,8 +77,16 @@ export const getMeAsync = ({shouldResetMe = false} = {}) => async dispatch => { logoutLink, }) ) + dispatch( - linksReceived({external, users, organizations, me: meLink, config}) + linksReceived({ + external, + users, + organizations, + me: meLink, + config, + environment, + }) ) // TODO: put this before meGetCompleted... though for some reason it doesn't fire the first time then } catch (error) { dispatch(meGetFailed()) diff --git a/ui/src/shared/apis/env.js b/ui/src/shared/apis/env.js new file mode 100644 index 0000000000..77070e9c86 --- /dev/null +++ b/ui/src/shared/apis/env.js @@ -0,0 +1,19 @@ +import AJAX from 'src/utils/ajax' + +const DEFAULT_ENVS = { + telegrafSystemInterval: '1m', +} + +export const getEnv = async url => { + try { + const {data} = await AJAX({ + method: 'GET', + url, + }) + + return data + } catch (error) { + console.error('Error retreieving envs: ', error) + return DEFAULT_ENVS + } +} diff --git a/ui/src/utils/ajax.js b/ui/src/utils/ajax.js index 232fa81255..48e9591ef2 100644 --- a/ui/src/utils/ajax.js +++ b/ui/src/utils/ajax.js @@ -18,7 +18,9 @@ const generateResponseWithLinks = (response, newLinks) => { organizations, me: meLink, config, + environment, } = newLinks + return { ...response, auth: {links: auth}, @@ -28,6 +30,7 @@ const generateResponseWithLinks = (response, newLinks) => { organizations, meLink, config, + environment, } }