Merge pull request #2672 from influxdata/feature/env

ENV variable TELEGRAF_SYSTEM_INTERVAL
pull/10616/head
Andrew Watkins 2018-01-04 12:37:31 -08:00 committed by GitHub
commit af4e525aaf
13 changed files with 237 additions and 54 deletions

View File

@ -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"`
}

27
server/env.go Normal file
View File

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

70
server/env_test.go Normal file
View File

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

View File

@ -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,

View File

@ -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{

View File

@ -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))

View File

@ -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").

View File

@ -15,6 +15,7 @@ type Service struct {
TimeSeriesClient TimeSeriesClient
Logger chronograf.Logger
UseAuth bool
Env chronograf.Environment
Databases chronograf.Databases
}

View File

@ -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),

View File

@ -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)

View File

@ -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())

19
ui/src/shared/apis/env.js Normal file
View File

@ -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
}
}

View File

@ -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,
}
}