From 1b6724122c81fe16a601f32a1483a836df404497 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 19 Oct 2017 22:48:31 -0500 Subject: [PATCH 01/38] Add insecure ssl support to connect to kapacitor --- chronograf.go | 15 ++++---- kapacitor/client.go | 49 +++++++++++++------------ kapacitor/client_test.go | 38 ++++++++++---------- server/kapacitors.go | 76 +++++++++++++++++++++------------------ server/kapacitors_test.go | 8 +++++ server/swagger.json | 6 ++++ 6 files changed, 109 insertions(+), 83 deletions(-) diff --git a/chronograf.go b/chronograf.go index 89ed7bbc07..5e37ea8f2f 100644 --- a/chronograf.go +++ b/chronograf.go @@ -557,13 +557,14 @@ type KapacitorProperty struct { // Server represents a proxy connection to an HTTP server type Server struct { - ID int // ID is the unique ID of the server - SrcID int // SrcID of the data source - Name string // Name is the user-defined name for the server - Username string // Username is the username to connect to the server - Password string // Password is in CLEARTEXT - URL string // URL are the connections to the server - Active bool // Is this the active server for the source? + ID int // ID is the unique ID of the server + SrcID int // SrcID of the data source + Name string // Name is the user-defined name for the server + Username string // Username is the username to connect to the server + Password string // Password is in CLEARTEXT + URL string // URL are the connections to the server + InsecureSkipVerify bool // InsecureSkipVerify as true means any certificate presented by the server is accepted. + Active bool // Is this the active server for the source? } // ServersStore stores connection information for a `Server` diff --git a/kapacitor/client.go b/kapacitor/client.go index 1e7fbf9f51..da6eaa8a57 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -19,12 +19,13 @@ const ( // Client communicates to kapacitor type Client struct { - URL string - Username string - Password string - ID chronograf.ID - Ticker chronograf.Ticker - kapaClient func(url, username, password string) (KapaClient, error) + URL string + Username string + Password string + InsecureSkipVerify bool + ID chronograf.ID + Ticker chronograf.Ticker + kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) } // KapaClient represents a connection to a kapacitor instance @@ -37,14 +38,15 @@ type KapaClient interface { } // NewClient creates a client that interfaces with Kapacitor tasks -func NewClient(url, username, password string) *Client { +func NewClient(url, username, password string, insecureSkipVerify bool) *Client { return &Client{ - URL: url, - Username: username, - Password: password, - ID: &uuid.V4{}, - Ticker: &Alert{}, - kapaClient: NewKapaClient, + URL: url, + Username: username, + Password: password, + InsecureSkipVerify: insecureSkipVerify, + ID: &uuid.V4{}, + Ticker: &Alert{}, + kapaClient: NewKapaClient, } } @@ -121,7 +123,7 @@ func (c *Client) Create(ctx context.Context, rule chronograf.AlertRule) (*Task, return nil, err } - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return nil, err } @@ -189,7 +191,7 @@ func (c *Client) createFromQueryConfig(rule chronograf.AlertRule) (*client.Creat // Delete removes tickscript task from kapacitor func (c *Client) Delete(ctx context.Context, href string) error { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return err } @@ -197,7 +199,7 @@ func (c *Client) Delete(ctx context.Context, href string) error { } func (c *Client) updateStatus(ctx context.Context, href string, status client.TaskStatus) (*Task, error) { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return nil, err } @@ -235,7 +237,7 @@ func (c *Client) Status(ctx context.Context, href string) (string, error) { } func (c *Client) status(ctx context.Context, href string) (client.TaskStatus, error) { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return 0, err } @@ -249,7 +251,7 @@ func (c *Client) status(ctx context.Context, href string) (client.TaskStatus, er // All returns all tasks in kapacitor func (c *Client) All(ctx context.Context) (map[string]*Task, error) { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return nil, err } @@ -286,7 +288,7 @@ func (c *Client) Reverse(id string, script chronograf.TICKScript) chronograf.Ale // Get returns a single alert in kapacitor func (c *Client) Get(ctx context.Context, id string) (*Task, error) { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return nil, err } @@ -301,7 +303,7 @@ func (c *Client) Get(ctx context.Context, id string) (*Task, error) { // Update changes the tickscript of a given id. func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertRule) (*Task, error) { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify) if err != nil { return nil, err } @@ -386,7 +388,7 @@ func toTask(q *chronograf.QueryConfig) client.TaskType { } // NewKapaClient creates a Kapacitor client connection -func NewKapaClient(url, username, password string) (KapaClient, error) { +func NewKapaClient(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { var creds *client.Credentials if username != "" { creds = &client.Credentials{ @@ -397,8 +399,9 @@ func NewKapaClient(url, username, password string) (KapaClient, error) { } clnt, err := client.New(client.Config{ - URL: url, - Credentials: creds, + URL: url, + Credentials: creds, + InsecureSkipVerify: insecureSkipVerify, }) if err != nil { diff --git a/kapacitor/client_test.go b/kapacitor/client_test.go index 1932b1a6c9..29b7805424 100644 --- a/kapacitor/client_test.go +++ b/kapacitor/client_test.go @@ -75,7 +75,7 @@ func TestClient_All(t *testing.T) { Password string ID chronograf.ID Ticker chronograf.Ticker - kapaClient func(url, username, password string) (KapaClient, error) + kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) } type args struct { ctx context.Context @@ -100,7 +100,7 @@ func TestClient_All(t *testing.T) { { name: "return no tasks", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, }, @@ -110,7 +110,7 @@ func TestClient_All(t *testing.T) { { name: "return a non-reversible task", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, }, @@ -141,7 +141,7 @@ func TestClient_All(t *testing.T) { { name: "return a reversible task", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, }, @@ -380,7 +380,7 @@ func TestClient_Get(t *testing.T) { Password string ID chronograf.ID Ticker chronograf.Ticker - kapaClient func(url, username, password string) (KapaClient, error) + kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) } type args struct { ctx context.Context @@ -406,7 +406,7 @@ func TestClient_Get(t *testing.T) { { name: "return no task", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, }, @@ -423,7 +423,7 @@ func TestClient_Get(t *testing.T) { { name: "return non-reversible task", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, }, @@ -465,7 +465,7 @@ func TestClient_Get(t *testing.T) { { name: "return reversible task", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, }, @@ -706,7 +706,7 @@ func TestClient_updateStatus(t *testing.T) { Password string ID chronograf.ID Ticker chronograf.Ticker - kapaClient func(url, username, password string) (KapaClient, error) + kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) } type args struct { ctx context.Context @@ -727,7 +727,7 @@ func TestClient_updateStatus(t *testing.T) { { name: "disable alert rule", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -777,7 +777,7 @@ func TestClient_updateStatus(t *testing.T) { { name: "fail to enable alert rule", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -797,7 +797,7 @@ func TestClient_updateStatus(t *testing.T) { { name: "enable alert rule", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -880,7 +880,7 @@ func TestClient_Update(t *testing.T) { Password string ID chronograf.ID Ticker chronograf.Ticker - kapaClient func(url, username, password string) (KapaClient, error) + kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) } type args struct { ctx context.Context @@ -902,7 +902,7 @@ func TestClient_Update(t *testing.T) { { name: "update alert rule error", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -936,7 +936,7 @@ func TestClient_Update(t *testing.T) { { name: "update alert rule", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -1000,7 +1000,7 @@ func TestClient_Update(t *testing.T) { { name: "stays disabled when already disabled", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -1099,7 +1099,7 @@ func TestClient_Create(t *testing.T) { Password string ID chronograf.ID Ticker chronograf.Ticker - kapaClient func(url, username, password string) (KapaClient, error) + kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) } type args struct { ctx context.Context @@ -1119,7 +1119,7 @@ func TestClient_Create(t *testing.T) { { name: "create alert rule", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, @@ -1185,7 +1185,7 @@ func TestClient_Create(t *testing.T) { { name: "create alert rule error", fields: fields{ - kapaClient: func(url, username, password string) (KapaClient, error) { + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { return kapa, nil }, Ticker: &Alert{}, diff --git a/server/kapacitors.go b/server/kapacitors.go index e7982a8cb3..fde467a743 100644 --- a/server/kapacitors.go +++ b/server/kapacitors.go @@ -12,11 +12,12 @@ import ( ) type postKapacitorRequest struct { - Name *string `json:"name"` // User facing name of kapacitor instance.; Required: true - URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true - Username string `json:"username,omitempty"` // Username for authentication to kapacitor - Password string `json:"password,omitempty"` - Active bool `json:"active"` + Name *string `json:"name"` // User facing name of kapacitor instance.; Required: true + URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true + Username string `json:"username,omitempty"` // Username for authentication to kapacitor + Password string `json:"password,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. + Active bool `json:"active"` } func (p *postKapacitorRequest) Valid() error { @@ -44,13 +45,14 @@ type kapaLinks struct { } type kapacitor struct { - ID int `json:"id,string"` // Unique identifier representing a kapacitor instance. - Name string `json:"name"` // User facing name of kapacitor instance. - URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092) - Username string `json:"username,omitempty"` // Username for authentication to kapacitor - Password string `json:"password,omitempty"` - Active bool `json:"active"` - Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor + ID int `json:"id,string"` // Unique identifier representing a kapacitor instance. + Name string `json:"name"` // User facing name of kapacitor instance. + URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092) + Username string `json:"username,omitempty"` // Username for authentication to kapacitor + Password string `json:"password,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. + Active bool `json:"active"` + Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor } // NewKapacitor adds valid kapacitor store store. @@ -79,12 +81,13 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) { } srv := chronograf.Server{ - SrcID: srcID, - Name: *req.Name, - Username: req.Username, - Password: req.Password, - URL: *req.URL, - Active: req.Active, + SrcID: srcID, + Name: *req.Name, + Username: req.Username, + Password: req.Password, + InsecureSkipVerify: req.InsecureSkipVerify, + URL: *req.URL, + Active: req.Active, } if srv, err = h.ServersStore.Add(ctx, srv); err != nil { @@ -101,11 +104,12 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) { func newKapacitor(srv chronograf.Server) kapacitor { httpAPISrcs := "/chronograf/v1/sources" return kapacitor{ - ID: srv.ID, - Name: srv.Name, - Username: srv.Username, - URL: srv.URL, - Active: srv.Active, + ID: srv.ID, + Name: srv.Name, + Username: srv.Username, + URL: srv.URL, + Active: srv.Active, + InsecureSkipVerify: srv.InsecureSkipVerify, Links: kapaLinks{ Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID), Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID), @@ -204,11 +208,12 @@ func (h *Service) RemoveKapacitor(w http.ResponseWriter, r *http.Request) { } type patchKapacitorRequest struct { - Name *string `json:"name,omitempty"` // User facing name of kapacitor instance. - URL *string `json:"url,omitempty"` // URL for the kapacitor - Username *string `json:"username,omitempty"` // Username for kapacitor auth - Password *string `json:"password,omitempty"` - Active *bool `json:"active"` + Name *string `json:"name,omitempty"` // User facing name of kapacitor instance. + URL *string `json:"url,omitempty"` // URL for the kapacitor + Username *string `json:"username,omitempty"` // Username for kapacitor auth + Password *string `json:"password,omitempty"` + InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted. + Active *bool `json:"active"` } func (p *patchKapacitorRequest) Valid() error { @@ -268,6 +273,9 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) { if req.Username != nil { srv.Username = *req.Username } + if req.InsecureSkipVerify != nil { + srv.InsecureSkipVerify = *req.InsecureSkipVerify + } if req.Active != nil { srv.Active = *req.Active } @@ -303,7 +311,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) { return } - c := kapa.NewClient(srv.URL, srv.Username, srv.Password) + c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) var req chronograf.AlertRule if err = json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -440,7 +448,7 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) { } tid := httprouter.GetParamFromContext(ctx, "tid") - c := kapa.NewClient(srv.URL, srv.Username, srv.Password) + c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) var req chronograf.AlertRule if err = json.NewDecoder(r.Body).Decode(&req); err != nil { invalidJSON(w, h.Logger) @@ -510,7 +518,7 @@ func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) { } tid := httprouter.GetParamFromContext(ctx, "tid") - c := kapa.NewClient(srv.URL, srv.Username, srv.Password) + c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) var req KapacitorStatus if err = json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -570,7 +578,7 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) { return } - c := kapa.NewClient(srv.URL, srv.Username, srv.Password) + c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) tasks, err := c.All(ctx) if err != nil { Error(w, http.StatusInternalServerError, err.Error(), h.Logger) @@ -613,7 +621,7 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) { } tid := httprouter.GetParamFromContext(ctx, "tid") - c := kapa.NewClient(srv.URL, srv.Username, srv.Password) + c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) // Check if the rule exists within scope task, err := c.Get(ctx, tid) @@ -651,7 +659,7 @@ func (h *Service) KapacitorRulesDelete(w http.ResponseWriter, r *http.Request) { return } - c := kapa.NewClient(srv.URL, srv.Username, srv.Password) + c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify) tid := httprouter.GetParamFromContext(ctx, "tid") // Check if the rule is linked to this server and kapacitor diff --git a/server/kapacitors_test.go b/server/kapacitors_test.go index 2a55ea637c..b6f4751710 100644 --- a/server/kapacitors_test.go +++ b/server/kapacitors_test.go @@ -174,6 +174,14 @@ func Test_KapacitorRulesGet(t *testing.T) { // setup mock service and test logger testLogger := mocks.TestLogger{} svc := &server.Service{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: ID, + InsecureSkipVerify: true, + }, nil + }, + }, ServersStore: &mocks.ServersStore{ GetF: func(ctx context.Context, ID int) (chronograf.Server, error) { return chronograf.Server{ diff --git a/server/swagger.json b/server/swagger.json index 11c40d9060..58ebff54e2 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -2357,6 +2357,7 @@ "name": "kapa", "url": "http://localhost:9092", "active": false, + "insecureSkipVerify": false, "links": { "proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy", "self": "/chronograf/v1/sources/4/kapacitors/4", @@ -2387,6 +2388,11 @@ "description": "URL for the kapacitor backend (e.g. http://localhost:9092)" }, + "insecureSkipVerify": { + "type": "boolean", + "description": + "True means any certificate presented by the kapacitor is accepted. Typically used for self-signed certs. Probably should only be used for testing." + }, "active": { "type": "boolean", "description": From b9d6d4ef080b0b7c88ee78088a5828de9fc05187 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 24 Oct 2017 13:39:16 -0700 Subject: [PATCH 02/38] Fix error to console when range is zero for dygraph logscale --- ui/src/shared/components/Dygraph.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index 43f21725c4..95caa9efa6 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -43,7 +43,6 @@ export default class Dygraph extends Component { componentDidMount() { const { axes: {y, y2}, - ruleValues, isGraphFilled: fillGraph, isBarGraph, options, @@ -63,9 +62,7 @@ export default class Dygraph extends Component { plugins: [new Dygraphs.Plugins.Crosshair({direction: 'vertical'})], axes: { y: { - valueRange: options.stackedGraph - ? getStackedRange(y.bounds) - : getRange(timeSeries, y.bounds, ruleValues), + valueRange: this.getYRange(timeSeries), axisLabelFormatter: (yval, __, opts) => numberValueFormatter(yval, opts, y.prefix, y.suffix), axisLabelWidth: this.getLabelWidth(), @@ -130,7 +127,7 @@ export default class Dygraph extends Component { } componentDidUpdate() { - const {labels, axes: {y, y2}, options, ruleValues, isBarGraph} = this.props + const {labels, axes: {y, y2}, options, isBarGraph} = this.props const dygraph = this.dygraph if (!dygraph) { @@ -149,9 +146,7 @@ export default class Dygraph extends Component { ylabel: this.getLabel('y'), axes: { y: { - valueRange: options.stackedGraph - ? getStackedRange(y.bounds) - : getRange(timeSeries, y.bounds, ruleValues), + valueRange: this.getYRange(timeSeries), axisLabelFormatter: (yval, __, opts) => numberValueFormatter(yval, opts, y.prefix, y.suffix), axisLabelWidth: this.getLabelWidth(), @@ -176,6 +171,24 @@ export default class Dygraph extends Component { this.props.setResolution(w) } + getYRange = timeSeries => { + const {options, axes: {y}, ruleValues} = this.props + + if (options.stackedGraph) { + return getStackedRange(y.bounds) + } + + const range = getRange(timeSeries, y.bounds, ruleValues) + const [min, max] = range + + // Bug in Dygraph calculates a negative range for logscale when min range is 0 + if (y.scale === LOG && min <= 0) { + return [0.1, max] + } + + return range + } + handleZoom = (lower, upper) => { const {onZoom} = this.props From 843982819d58a4b572752fa81e7a81db602bc519 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 24 Oct 2017 13:39:50 -0700 Subject: [PATCH 03/38] Prevent user from deprecating number to below zero if logscale --- ui/src/dashboards/components/AxesOptions.js | 3 +++ ui/src/shared/components/ClickOutsideInput.js | 3 +++ ui/src/shared/components/Dygraph.js | 2 +- ui/src/shared/components/OptIn.js | 7 ++++--- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index d44640ea78..fcce69a055 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -6,6 +6,7 @@ import {Tabber, Tab} from 'src/dashboards/components/Tabber' import {DISPLAY_OPTIONS, TOOLTIP_CONTENT} from 'src/dashboards/constants' const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS +const getInputMin = scale => (scale === LOG ? '0' : null) const AxesOptions = ({ axes: {y: {bounds, label, prefix, suffix, base, scale, defaultYLabel}}, @@ -38,6 +39,7 @@ const AxesOptions = ({ customValue={min} onSetValue={onSetYAxisBoundMin} type="number" + min={getInputMin(scale)} />
@@ -47,6 +49,7 @@ const AxesOptions = ({ customValue={max} onSetValue={onSetYAxisBoundMax} type="number" + min={getInputMin(scale)} />
(this.customValueInput = el) render() { - const {fixedPlaceholder, customPlaceholder, type} = this.props + const {fixedPlaceholder, customPlaceholder, type, min} = this.props const {useCustomValue, customValue} = this.state return ( @@ -110,6 +110,7 @@ class OptIn extends Component { > -
Date: Tue, 24 Oct 2017 13:49:31 -0700 Subject: [PATCH 04/38] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2615c899d8..d6269b676b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.3.11.0 [unreleased] ### Bug Fixes +1. [#2157](https://github.com/influxdata/chronograf/pull/2157): Fix logscale producing console errors when only one point in graph ### Features ### UI Improvements From 28f298e5784420c95f55646c02d551e31ea90ccd Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 24 Oct 2017 14:28:37 -0700 Subject: [PATCH 05/38] Fix cannot connect to source false error flag --- ui/src/dashboards/actions/index.js | 3 ++- ui/src/shared/parsing/index.js | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index b151b5e19e..0108e3ab96 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -281,7 +281,8 @@ export const updateTempVarValues = (source, dashboard) => async dispatch => { results.forEach(({data}, i) => { const {type, query, id} = tempsWithQueries[i] - const vals = parsers[type](data, query.tagKey || query.measurement)[type] + const parsed = parsers[type](data, query.tagKey || query.measurement) + const vals = parsed[type] dispatch(editTemplateVariableValues(dashboard.id, id, vals)) }) } catch (error) { diff --git a/ui/src/shared/parsing/index.js b/ui/src/shared/parsing/index.js index 7ac11b03bf..5ea98db42a 100644 --- a/ui/src/shared/parsing/index.js +++ b/ui/src/shared/parsing/index.js @@ -1,3 +1,4 @@ +import _ from 'lodash' import databases from 'shared/parsing/showDatabases' import measurements from 'shared/parsing/showMeasurements' import fieldKeys from 'shared/parsing/showFieldKeys' @@ -8,16 +9,19 @@ const parsers = { databases, measurements: data => { const {errors, measurementSets} = measurements(data) - return {errors, measurements: measurementSets[0].measurements} + return { + errors, + measurements: _.get(measurementSets, ['0', 'measurements'], []), + } }, fieldKeys: (data, key) => { const {errors, fieldSets} = fieldKeys(data) - return {errors, fieldKeys: fieldSets[key]} + return {errors, fieldKeys: _.get(fieldSets, key, [])} }, tagKeys, tagValues: (data, key) => { const {errors, tags} = tagValues(data) - return {errors, tagValues: tags[key]} + return {errors, tagValues: _.get(tags, key, [])} }, } From 6c977549dfadda2b69367207dca6091e6f220d3d Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 24 Oct 2017 14:38:57 -0700 Subject: [PATCH 06/38] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2615c899d8..a797bea68c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.3.11.0 [unreleased] ### Bug Fixes +1. [#2158](https://github.com/influxdata/chronograf/pull/2158): Fix 'Cannot connect to source' false error flag on Dashboard page ### Features ### UI Improvements From f69341dd9fd92181020084f9c0a84a307ef9811e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 25 Oct 2017 09:48:10 -0700 Subject: [PATCH 07/38] Update database list to include retention --- ui/src/shared/components/DatabaseList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/components/DatabaseList.js b/ui/src/shared/components/DatabaseList.js index 076cbdfdc6..3b20a501d5 100644 --- a/ui/src/shared/components/DatabaseList.js +++ b/ui/src/shared/components/DatabaseList.js @@ -97,7 +97,7 @@ const DatabaseList = React.createClass({ return (
-
Databases
+
DB.RetentionPolicy
{sortedNamespaces.map(namespace => { From 4eb8f8945d831d1ef74be667aceab321e8708294 Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Thu, 26 Oct 2017 12:52:36 -0700 Subject: [PATCH 08/38] Adds fractions of seconds to time field in csv export --- ui/src/shared/parsing/resultsToCSV.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/parsing/resultsToCSV.js b/ui/src/shared/parsing/resultsToCSV.js index 8ba25b2d53..c3d3fdd020 100644 --- a/ui/src/shared/parsing/resultsToCSV.js +++ b/ui/src/shared/parsing/resultsToCSV.js @@ -2,7 +2,7 @@ import _ from 'lodash' import moment from 'moment' export const formatDate = timestamp => - moment(timestamp).format('M/D/YYYY h:mm:ss A') + moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A') export const resultsToCSV = results => { if (!_.get(results, ['0', 'series', '0'])) { From 30cf093a50ff9cc9fe4ec06013ced0146e4e39da Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 26 Oct 2017 14:07:38 -0700 Subject: [PATCH 09/38] Highlight circles are back --- ui/src/external/dygraph.js | 2 + ui/src/shared/components/Dygraph.js | 72 +++-------------------- ui/src/shared/components/DygraphLegend.js | 16 +++-- ui/src/shared/graphs/helpers.js | 59 +++++++++++++++++++ 4 files changed, 82 insertions(+), 67 deletions(-) diff --git a/ui/src/external/dygraph.js b/ui/src/external/dygraph.js index e46364b2e7..597e42077c 100644 --- a/ui/src/external/dygraph.js +++ b/ui/src/external/dygraph.js @@ -283,11 +283,13 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { minYDist = ydist closestRow = point.idx closestSeries = setIdx + closestPoint = point } else if (xdist === minXDist && ydist < minYDist) { minXDist = xdist minYDist = ydist closestRow = point.idx closestSeries = setIdx + closestPoint = point } } } diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index 43f21725c4..b227e5a78d 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -29,6 +29,7 @@ export default class Dygraph extends Component { x: null, series: [], }, + pageX: null, sortType: '', filterText: '', isSynced: false, @@ -36,7 +37,6 @@ export default class Dygraph extends Component { isAscending: true, isSnipped: false, isFilterVisible: false, - legendArrowPosition: 'top', } } @@ -171,9 +171,8 @@ export default class Dygraph extends Component { dygraph.updateOptions(updateOptions) const {w} = this.dygraph.getArea() - this.resize() - this.dygraph.resize() this.props.setResolution(w) + this.resize() } handleZoom = (lower, upper) => { @@ -298,6 +297,7 @@ export default class Dygraph extends Component { resize = () => { this.dygraph.resizeElements_() this.dygraph.predraw_() + this.dygraph.resize() } formatTimeRange = timeRange => { @@ -341,64 +341,8 @@ export default class Dygraph extends Component { } } - highlightCallback = e => { - const chronografChromeSize = 60 // Width & Height of navigation page elements - - // Move the Legend on hover - const graphRect = this.graphRef.getBoundingClientRect() - const legendRect = this.legendRef.getBoundingClientRect() - - const graphWidth = graphRect.width + 32 // Factoring in padding from parent - const graphHeight = graphRect.height - const graphBottom = graphRect.bottom - const legendWidth = legendRect.width - const legendHeight = legendRect.height - const screenHeight = window.innerHeight - const legendMaxLeft = graphWidth - legendWidth / 2 - const trueGraphX = e.pageX - graphRect.left - - let legendLeft = trueGraphX - - // Enforcing max & min legend offsets - if (trueGraphX < legendWidth / 2) { - legendLeft = legendWidth / 2 - } else if (trueGraphX > legendMaxLeft) { - legendLeft = legendMaxLeft - } - - // Disallow screen overflow of legend - const isLegendBottomClipped = graphBottom + legendHeight > screenHeight - const isLegendTopClipped = - legendHeight > graphRect.top - chronografChromeSize - const willLegendFitLeft = e.pageX - chronografChromeSize > legendWidth - - let legendTop = graphHeight + 8 - this.setState({legendArrowPosition: 'top'}) - - // If legend is only clipped on the bottom, position above graph - if (isLegendBottomClipped && !isLegendTopClipped) { - this.setState({legendArrowPosition: 'bottom'}) - legendTop = -legendHeight - } - // If legend is clipped on top and bottom, posiition on either side of crosshair - if (isLegendBottomClipped && isLegendTopClipped) { - legendTop = 0 - - if (willLegendFitLeft) { - this.setState({legendArrowPosition: 'right'}) - legendLeft = trueGraphX - legendWidth / 2 - legendLeft -= 8 - } else { - this.setState({legendArrowPosition: 'left'}) - legendLeft = trueGraphX + legendWidth / 2 - legendLeft += 32 - } - } - - this.legendRef.style.left = `${legendLeft}px` - this.legendRef.style.top = `${legendTop}px` - - this.setState({isHidden: false}) + highlightCallback = ({pageX}) => { + this.setState({isHidden: false, pageX}) } legendFormatter = legend => { @@ -424,12 +368,12 @@ export default class Dygraph extends Component { render() { const { legend, + pageX, sortType, isHidden, isSnipped, filterText, isAscending, - legendArrowPosition, isFilterVisible, } = this.state @@ -437,6 +381,9 @@ export default class Dygraph extends Component {
{ diff --git a/ui/src/shared/components/DygraphLegend.js b/ui/src/shared/components/DygraphLegend.js index f0d7c79ea9..6d8bf619df 100644 --- a/ui/src/shared/components/DygraphLegend.js +++ b/ui/src/shared/components/DygraphLegend.js @@ -2,6 +2,8 @@ import React, {PropTypes} from 'react' import _ from 'lodash' import classnames from 'classnames' +import {makeLegendStyles} from 'shared/graphs/helpers' + const removeMeasurement = (label = '') => { const [measurement] = label.match(/^(.*)[.]/g) || [''] return label.replace(measurement, '') @@ -9,6 +11,9 @@ const removeMeasurement = (label = '') => { const DygraphLegend = ({ xHTML, + pageX, + graph, + legend, series, onSort, onSnip, @@ -20,7 +25,6 @@ const DygraphLegend = ({ filterText, isAscending, onInputChange, - arrowPosition, isFilterVisible, onToggleFilter, }) => { @@ -28,9 +32,11 @@ const DygraphLegend = ({ series, ({y, label}) => (sortType === 'numeric' ? y : label) ) + const ordered = isAscending ? sorted : sorted.reverse() const filtered = ordered.filter(s => s.label.match(filterText)) const hidden = isHidden ? 'hidden' : '' + const style = makeLegendStyles(graph, legend, pageX) const renderSortAlpha = (
@@ -141,7 +148,9 @@ DygraphLegend.propTypes = { yHTML: string, }) ), - dygraph: shape(), + pageX: number, + legend: shape({}), + graph: shape({}), onSnip: func.isRequired, onHide: func.isRequired, onSort: func.isRequired, @@ -154,7 +163,6 @@ DygraphLegend.propTypes = { legendRef: func.isRequired, isSnipped: bool.isRequired, isFilterVisible: bool.isRequired, - arrowPosition: string.isRequired, } export default DygraphLegend diff --git a/ui/src/shared/graphs/helpers.js b/ui/src/shared/graphs/helpers.js index e210d36f58..082f7e0493 100644 --- a/ui/src/shared/graphs/helpers.js +++ b/ui/src/shared/graphs/helpers.js @@ -114,6 +114,65 @@ export const barPlotter = e => { } } +export const makeLegendStyles = (graph, legend, pageX) => { + if (!graph || !legend || pageX === null) { + return {} + } + + // Move the Legend on hover + const chronografChromeSize = 60 // Width & Height of navigation page elements + const graphRect = graph.getBoundingClientRect() + const legendRect = legend.getBoundingClientRect() + + const graphWidth = graphRect.width + 32 // Factoring in padding from parent + const graphHeight = graphRect.height + const graphBottom = graphRect.bottom + const legendWidth = legendRect.width + const legendHeight = legendRect.height + const screenHeight = window.innerHeight + const legendMaxLeft = graphWidth - legendWidth / 2 + const trueGraphX = pageX - graphRect.left + + let legendLeft = trueGraphX + + // Enforcing max & min legend offsets + if (trueGraphX < legendWidth / 2) { + legendLeft = legendWidth / 2 + } else if (trueGraphX > legendMaxLeft) { + legendLeft = legendMaxLeft + } + + // Disallow screen overflow of legend + const isLegendBottomClipped = graphBottom + legendHeight > screenHeight + const isLegendTopClipped = legendHeight > graphRect.top - chronografChromeSize + const willLegendFitLeft = pageX - chronografChromeSize > legendWidth + + let legendTop = graphHeight + 8 + + // If legend is only clipped on the bottom, position above graph + if (isLegendBottomClipped && !isLegendTopClipped) { + legendTop = -legendHeight + } + + // If legend is clipped on top and bottom, posiition on either side of crosshair + if (isLegendBottomClipped && isLegendTopClipped) { + legendTop = 0 + + if (willLegendFitLeft) { + legendLeft = trueGraphX - legendWidth / 2 + legendLeft -= 8 + } else { + legendLeft = trueGraphX + legendWidth / 2 + legendLeft += 32 + } + } + + return { + left: `${legendLeft}px`, + top: `${legendTop}px`, + } +} + export const OPTIONS = { rightGap: 0, axisLineWidth: 2, From 1fabda0156e135fb52ab327e6b047dda4f8695ef Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Thu, 26 Oct 2017 14:35:03 -0700 Subject: [PATCH 10/38] Fix tests --- ui/spec/shared/parsing/resultsToCSVSpec.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/spec/shared/parsing/resultsToCSVSpec.js b/ui/spec/shared/parsing/resultsToCSVSpec.js index 6945e713db..aa8199566a 100644 --- a/ui/spec/shared/parsing/resultsToCSVSpec.js +++ b/ui/spec/shared/parsing/resultsToCSVSpec.js @@ -3,13 +3,16 @@ import { formatDate, dashboardtoCSV, } from 'shared/parsing/resultsToCSV' +import moment from 'moment' describe('formatDate', () => { it('converts timestamp to an excel compatible date string', () => { const timestamp = 1000000000000 const result = formatDate(timestamp) expect(result).to.be.a('string') - expect(+new Date(result)).to.equal(timestamp) + expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).to.equal( + timestamp + ) }) }) From f4007cb2b1c8a8d5daf519b233c8b4d0cbc107db Mon Sep 17 00:00:00 2001 From: deniz kusefoglu Date: Thu, 26 Oct 2017 14:37:23 -0700 Subject: [PATCH 11/38] Edit changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2615c899d8..7d930ec4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## v1.3.11.0 [unreleased] ### Bug Fixes +1. [#2167](https://github.com/influxdata/chronograf/pull/2167): Add fractions of seconds to time field in csv export ### Features ### UI Improvements From 10e45721b4f5947a2dc24b8f102bd0b24044838f Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 26 Oct 2017 17:37:28 -0500 Subject: [PATCH 12/38] Add flush interval to kapacitor proxy to fix buffering --- server/proxy.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server/proxy.go b/server/proxy.go index b00709e072..b05f08645f 100644 --- a/server/proxy.go +++ b/server/proxy.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "time" ) // KapacitorProxy proxies requests to kapacitor using the path query parameter. @@ -54,8 +55,12 @@ func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) { req.SetBasicAuth(srv.Username, srv.Password) } } + + // Without a FlushInterval the HTTP Chunked response for kapacitor logs is + // buffered and flushed every 30 seconds. proxy := &httputil.ReverseProxy{ - Director: director, + Director: director, + FlushInterval: time.Second, } proxy.ServeHTTP(w, r) } From f94eed49f5b7761d0c3206833adb0210232a8cba Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 26 Oct 2017 17:38:03 -0500 Subject: [PATCH 13/38] Fix kapacitor proxy to accept url query parameters --- server/proxy.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/server/proxy.go b/server/proxy.go index b05f08645f..3ba5713c59 100644 --- a/server/proxy.go +++ b/server/proxy.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strings" "time" ) @@ -35,20 +36,21 @@ func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) { return } - u, err := url.Parse(srv.URL) + // To preserve any HTTP query arguments to the kapacitor path, + // we concat and parse them into u. + uri := singleJoiningSlash(srv.URL, path) + u, err := url.Parse(uri) if err != nil { msg := fmt.Sprintf("Error parsing kapacitor url: %v", err) Error(w, http.StatusUnprocessableEntity, msg, h.Logger) return } - u.Path = path - director := func(req *http.Request) { // Set the Host header of the original Kapacitor URL req.Host = u.Host - req.URL = u + // Because we are acting as a proxy, kapacitor needs to have the basic auth information set as // a header directly if srv.Username != "" && srv.Password != "" { @@ -84,3 +86,15 @@ func (h *Service) KapacitorProxyGet(w http.ResponseWriter, r *http.Request) { func (h *Service) KapacitorProxyDelete(w http.ResponseWriter, r *http.Request) { h.KapacitorProxy(w, r) } + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + if aslash && bslash { + return a + b[1:] + } + if !aslash && !bslash { + return a + "/" + b + } + return a + b +} From 3152471f7cdafb46330b8fbcdeea2abdcd4b7f85 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 26 Oct 2017 17:38:20 -0500 Subject: [PATCH 14/38] Fix logger and redirector to be flushers allowing HTTP chunking --- server/logger.go | 44 ++++++++++++++++++++++++---------- server/prefixing_redirector.go | 22 ++++++++++++++--- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/server/logger.go b/server/logger.go index 81465aafab..3ca5ab24f9 100644 --- a/server/logger.go +++ b/server/logger.go @@ -7,39 +7,57 @@ import ( "github.com/influxdata/chronograf" ) -type logResponseWriter struct { +// statusWriterFlusher captures the status header of an http.ResponseWriter +// and is a flusher +type statusWriter struct { http.ResponseWriter - - responseCode int + Flusher http.Flusher + status int } -func (l *logResponseWriter) WriteHeader(status int) { - l.responseCode = status - l.ResponseWriter.WriteHeader(status) +func (w *statusWriter) WriteHeader(status int) { + w.status = status + w.ResponseWriter.WriteHeader(status) +} + +func (w *statusWriter) Status() int { return w.status } + +// Flush is here because the underlying HTTP chunked transfer response writer +// to implement http.Flusher. Without it data is silently buffered. This +// was discovered when proxying kapacitor chunked logs. +func (w *statusWriter) Flush() { + if w.Flusher != nil { + w.Flusher.Flush() + } } // Logger is middleware that logs the request func Logger(logger chronograf.Logger, next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { now := time.Now() - logger. - WithField("component", "server"). + logger.WithField("component", "server"). WithField("remote_addr", r.RemoteAddr). WithField("method", r.Method). WithField("url", r.URL). - Info("Request") + Debug("Request") - lrr := &logResponseWriter{w, 0} - next.ServeHTTP(lrr, r) + sw := &statusWriter{ + ResponseWriter: w, + } + if f, ok := w.(http.Flusher); ok { + sw.Flusher = f + } + next.ServeHTTP(sw, r) later := time.Now() elapsed := later.Sub(now) logger. WithField("component", "server"). WithField("remote_addr", r.RemoteAddr). + WithField("method", r.Method). WithField("response_time", elapsed.String()). - WithField("code", lrr.responseCode). - Info("Response: ", http.StatusText(lrr.responseCode)) + WithField("status", sw.Status()). + Info("Response: ", http.StatusText(sw.Status())) } return http.HandlerFunc(fn) } diff --git a/server/prefixing_redirector.go b/server/prefixing_redirector.go index 86f957efab..2c4652d870 100644 --- a/server/prefixing_redirector.go +++ b/server/prefixing_redirector.go @@ -9,7 +9,8 @@ import ( type interceptingResponseWriter struct { http.ResponseWriter - Prefix string + Flusher http.Flusher + Prefix string } func (i *interceptingResponseWriter) WriteHeader(status int) { @@ -25,11 +26,26 @@ func (i *interceptingResponseWriter) WriteHeader(status int) { i.ResponseWriter.WriteHeader(status) } -// PrefixingRedirector alters the Location header of downstream http.Handlers +// Flush is here because the underlying HTTP chunked transfer response writer +// to implement http.Flusher. Without it data is silently buffered. This +// was discovered when proxying kapacitor chunked logs. +func (i *interceptingResponseWriter) Flush() { + if i.Flusher != nil { + i.Flusher.Flush() + } +} + +// PrefixedRedirect alters the Location header of downstream http.Handlers // to include a specified prefix func PrefixedRedirect(prefix string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - iw := &interceptingResponseWriter{w, prefix} + iw := &interceptingResponseWriter{ + ResponseWriter: w, + Prefix: prefix, + } + if flusher, ok := w.(http.Flusher); ok { + iw.Flusher = flusher + } next.ServeHTTP(iw, r) }) } From d2dac8353f9ab97ac7ac888740bd4f1e9b4a8584 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 26 Oct 2017 19:04:23 -0500 Subject: [PATCH 15/38] Update Go and Node to 1.9.2 and 6.11.5 --- etc/Dockerfile_build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/Dockerfile_build b/etc/Dockerfile_build index 0150bfc47c..af8deb426f 100644 --- a/etc/Dockerfile_build +++ b/etc/Dockerfile_build @@ -18,7 +18,7 @@ RUN pip install boto requests python-jose --upgrade RUN gem install fpm # Install node -ENV NODE_VERSION v6.10.3 +ENV NODE_VERSION v6.11.5 RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \ tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \ rm -f node-${NODE_VERSION}-linux-x64.tar.gz @@ -35,7 +35,7 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ # Install go ENV GOPATH /root/go -ENV GO_VERSION 1.8.1 +ENV GO_VERSION 1.9.2 ENV GO_ARCH amd64 RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \ tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \ From 16efd2bea114f0cd177c43be0f6cec21a81095ad Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Fri, 27 Oct 2017 10:11:09 -0500 Subject: [PATCH 16/38] Update circle builds to use go 1.9.2 and node 6.11.5 --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 28a0cff3b8..597de93298 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ machine: services: - docker environment: - DOCKER_TAG: chronograf-20170516 + DOCKER_TAG: chronograf-20171027 dependencies: override: From cbe1ee990180a23ac072e068d5b64ceee410edbd Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 27 Oct 2017 09:10:14 -0700 Subject: [PATCH 17/38] Fix memoizer warning in production build --- ui/webpack/prodConfig.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/ui/webpack/prodConfig.js b/ui/webpack/prodConfig.js index 51b405526d..188a4566cf 100644 --- a/ui/webpack/prodConfig.js +++ b/ui/webpack/prodConfig.js @@ -1,14 +1,14 @@ /* eslint-disable no-var */ -var webpack = require('webpack'); -var path = require('path'); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); -var HtmlWebpackPlugin = require("html-webpack-plugin"); -var package = require('../package.json'); -var dependencies = package.dependencies; +var webpack = require('webpack') +var path = require('path') +var ExtractTextPlugin = require('extract-text-webpack-plugin') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var package = require('../package.json') +var dependencies = package.dependencies var config = { bail: true, - devtool: 'eval', + devtool: 'eval', entry: { app: path.resolve(__dirname, '..', 'src', 'index.js'), vendor: Object.keys(dependencies), @@ -28,6 +28,15 @@ var config = { }, }, module: { + noParse: [ + path.resolve( + __dirname, + '..', + 'node_modules', + 'memoizerific', + 'memoizerific.js' + ), + ], preLoaders: [ { test: /\.js$/, From 7772bce02f6bcd981a352e0d052c502e47675780 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 27 Oct 2017 09:11:03 -0700 Subject: [PATCH 18/38] Prettier webpack --- ui/webpack/prodConfig.js | 51 +++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/ui/webpack/prodConfig.js b/ui/webpack/prodConfig.js index 188a4566cf..1536d8c23e 100644 --- a/ui/webpack/prodConfig.js +++ b/ui/webpack/prodConfig.js @@ -51,15 +51,21 @@ var config = { }, { test: /\.scss$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader!resolve-url!sass?sourceMap'), + loader: ExtractTextPlugin.extract( + 'style-loader', + 'css-loader!sass-loader!resolve-url!sass?sourceMap' + ), }, { test: /\.css$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'), + loader: ExtractTextPlugin.extract( + 'style-loader', + 'css-loader!postcss-loader' + ), }, { - test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - loader : 'file', + test: /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + loader: 'file', }, { test: /\.js$/, @@ -83,10 +89,10 @@ var config = { }, }), new webpack.ProvidePlugin({ - $: "jquery", - jQuery: "jquery", + $: 'jquery', + jQuery: 'jquery', }), - new ExtractTextPlugin("chronograf.css"), + new ExtractTextPlugin('chronograf.css'), new HtmlWebpackPlugin({ template: path.resolve(__dirname, '..', 'src', 'index.template.html'), inject: 'body', @@ -95,21 +101,28 @@ var config = { }), new webpack.optimize.UglifyJsPlugin({ compress: { - warnings: false - } + warnings: false, + }, }), new webpack.optimize.CommonsChunkPlugin({ names: ['vendor', 'manifest'], }), - function() { /* Webpack does not exit with non-zero status if error. */ - this.plugin("done", function(stats) { - if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf("--watch") == -1) { - console.log(stats.compilation.errors.toString({ - colors: true - })); - process.exit(1); + function() { + /* Webpack does not exit with non-zero status if error. */ + this.plugin('done', function(stats) { + if ( + stats.compilation.errors && + stats.compilation.errors.length && + process.argv.indexOf('--watch') == -1 + ) { + console.log( + stats.compilation.errors.toString({ + colors: true, + }) + ) + process.exit(1) } - }); + }) }, new webpack.DefinePlugin({ VERSION: JSON.stringify(require('../package.json').version), @@ -117,6 +130,6 @@ var config = { ], postcss: require('./postcss'), target: 'web', -}; +} -module.exports = config; +module.exports = config From 810803aff6a713086a51ebc60ce5cdb1390fbf44 Mon Sep 17 00:00:00 2001 From: Nicholas Drone Date: Fri, 27 Oct 2017 18:33:19 -0400 Subject: [PATCH 19/38] Fixes #1077 by removing win_system and system from the select query. --- CHANGELOG.md | 1 + ui/src/hosts/apis/index.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d930ec4be..66c97bc517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v1.3.11.0 [unreleased] ### Bug Fixes 1. [#2167](https://github.com/influxdata/chronograf/pull/2167): Add fractions of seconds to time field in csv export +2. [#1077](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requires users to run Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page. ### Features ### UI Improvements diff --git a/ui/src/hosts/apis/index.js b/ui/src/hosts/apis/index.js index 7267496e30..39caf9fbf0 100644 --- a/ui/src/hosts/apis/index.js +++ b/ui/src/hosts/apis/index.js @@ -11,7 +11,7 @@ export function getCpuAndLoadForHosts(proxyLink, telegrafDB) { 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); - SHOW TAG VALUES FROM /win_system|system/ WITH KEY = "host"`, + SHOW TAG VALUES WITH KEY = "host";`, db: telegrafDB, }).then(resp => { const hosts = {} @@ -87,7 +87,7 @@ export async function getAllHosts(proxyLink, telegrafDB) { try { const resp = await proxy({ source: proxyLink, - query: 'show tag values from /win_system|system/ with key = "host"', + query: 'show tag values with key = "host"', db: telegrafDB, }) const hosts = {} From 97f3e6812ff38187ac1acc3279f0a5a2bda898db Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Mon, 30 Oct 2017 19:27:02 -0500 Subject: [PATCH 20/38] Update chronograf logrotate policy to not rotate if file is 0 bytes --- etc/scripts/logrotate | 1 + 1 file changed, 1 insertion(+) diff --git a/etc/scripts/logrotate b/etc/scripts/logrotate index 39ebe40881..f172c0dff6 100644 --- a/etc/scripts/logrotate +++ b/etc/scripts/logrotate @@ -5,4 +5,5 @@ dateext copytruncate compress + notifempty } From 4cafd07d33b10ace565e15fe9f49b80a2be04f3e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 2 Nov 2017 13:36:15 -0700 Subject: [PATCH 21/38] Remove 'No Results' as empty state for Dygraphs Returning No Results when no series returned cause a bunch of issues. Firstly, if the user zoomed in to a time that returned no data the graph would disappear and they would no longer be able to double-click the graph to zoom back out. Secondly, if ONE series returned no results the entire graph would disappear. --- ui/src/shared/components/AutoRefresh.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index 3bbf5f2064..1c430091ed 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -136,13 +136,6 @@ const AutoRefresh = ComposedComponent => { return this.renderFetching(timeSeries) } - if ( - !this._resultsForQuery(timeSeries) || - !this.state.lastQuerySuccessful - ) { - return this.renderNoResults() - } - return ( { ) } - renderNoResults = () => { - return ( -
-

No Results

-
- ) - } - _resultsForQuery = data => data.length ? data.every(({response}) => From 725e80f7e90c9ce69f94417680703b347e86d69e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 2 Nov 2017 13:38:08 -0700 Subject: [PATCH 22/38] Make legend items unique --- ui/src/shared/components/DygraphLegend.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/shared/components/DygraphLegend.js b/ui/src/shared/components/DygraphLegend.js index 6d8bf619df..f19a3bebc3 100644 --- a/ui/src/shared/components/DygraphLegend.js +++ b/ui/src/shared/components/DygraphLegend.js @@ -1,6 +1,7 @@ import React, {PropTypes} from 'react' import _ from 'lodash' import classnames from 'classnames' +import uuid from 'node-uuid' import {makeLegendStyles} from 'shared/graphs/helpers' @@ -68,7 +69,6 @@ const DygraphLegend = ({
9
) - return (
+
{isSnipped ? removeMeasurement(label) : label} From cdd2c826b90f787942925e42d199d0b79387ec23 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 2 Nov 2017 17:57:02 -0500 Subject: [PATCH 23/38] Fix rendering of templated queries to the /queries endpoint --- server/queries.go | 32 +++---- server/queries_test.go | 187 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 server/queries_test.go diff --git a/server/queries.go b/server/queries.go index 94897ce2a7..7966be49ec 100644 --- a/server/queries.go +++ b/server/queries.go @@ -12,16 +12,21 @@ import ( "github.com/influxdata/chronograf/influx/queries" ) +// QueryRequest is query that will be converted to a queryConfig type QueryRequest struct { - ID string `json:"id"` - Query string `json:"query"` + ID string `json:"id"` + Query string `json:"query"` +} + +// QueriesRequest converts all queries to queryConfigs with the help +// of the template variables +type QueriesRequest struct { + Queries []QueryRequest `json:"queries"` TemplateVars chronograf.TemplateVars `json:"tempVars,omitempty"` } -type QueriesRequest struct { - Queries []QueryRequest `json:"queries"` -} - +// QueryResponse is the return result of a QueryRequest including +// the raw query, the templated query, the queryConfig and the queryAST type QueryResponse struct { ID string `json:"id"` Query string `json:"query"` @@ -31,11 +36,12 @@ type QueryResponse struct { TemplateVars chronograf.TemplateVars `json:"tempVars,omitempty"` } +// QueriesResponse is the response for a QueriesRequest type QueriesResponse struct { Queries []QueryResponse `json:"queries"` } -// Queries parses InfluxQL and returns the JSON +// Queries parses InfluxQL and returns mostly importantly a structured QueryConfig func (s *Service) Queries(w http.ResponseWriter, r *http.Request) { srcID, err := paramID("id", r) if err != nil { @@ -66,12 +72,7 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) { Query: q.Query, } - query := q.Query - if len(q.TemplateVars) > 0 { - query = influx.TemplateReplace(query, q.TemplateVars) - qr.QueryTemplated = &query - } - + query := influx.TemplateReplace(q.Query, req.TemplateVars) qc := ToQueryConfig(query) if err := s.DefaultRP(ctx, &qc, &src); err != nil { Error(w, http.StatusBadRequest, err.Error(), s.Logger) @@ -83,9 +84,10 @@ func (s *Service) Queries(w http.ResponseWriter, r *http.Request) { qr.QueryAST = stmt } - if len(q.TemplateVars) > 0 { - qr.TemplateVars = q.TemplateVars + if len(req.TemplateVars) > 0 { + qr.TemplateVars = req.TemplateVars qr.QueryConfig.RawText = &qr.Query + qr.QueryTemplated = &query } qr.QueryConfig.ID = q.ID diff --git a/server/queries_test.go b/server/queries_test.go new file mode 100644 index 0000000000..3e409779fe --- /dev/null +++ b/server/queries_test.go @@ -0,0 +1,187 @@ +package server + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bouk/httprouter" + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/mocks" +) + +func TestService_Queries(t *testing.T) { + tests := []struct { + name string + SourcesStore chronograf.SourcesStore + ID string + w *httptest.ResponseRecorder + r *http.Request + want string + }{ + { + name: "bad json", + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: ID, + }, nil + }, + }, + ID: "1", + w: httptest.NewRecorder(), + r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`howdy`))), + want: `{"code":400,"message":"Unparsable JSON"}`, + }, + { + name: "bad id", + ID: "howdy", + w: httptest.NewRecorder(), + r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte{})), + want: `{"code":422,"message":"Error converting ID howdy"}`, + }, + { + name: "query with no template vars", + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: ID, + }, nil + }, + }, + ID: "1", + w: httptest.NewRecorder(), + r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{ + "queries": [ + { + "query": "SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time > now() - 1m", + "id": "82b60d37-251e-4afe-ac93-ca20a3642b11" + } + ]}`))), + want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM db.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"db","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":null,"range":{"upper":"","lower":"now() - 1m"}},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"db","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]}}]} +`, + }, + { + name: "query with unparsable query", + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: ID, + }, nil + }, + }, + ID: "1", + w: httptest.NewRecorder(), + r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{ + "queries": [ + { + "query": "SHOW DATABASES", + "id": "82b60d37-251e-4afe-ac93-ca20a3642b11" + } + ]}`))), + want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SHOW DATABASES","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"","measurement":"","retentionPolicy":"","fields":[],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SHOW DATABASES","range":null}}]} +`, + }, + { + name: "query with template vars", + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: ID, + }, nil + }, + }, + ID: "1", + w: httptest.NewRecorder(), + r: httptest.NewRequest("POST", "/queries", bytes.NewReader([]byte(`{ + "queries": [ + { + "query": "SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time > now() - 1m", + "id": "82b60d37-251e-4afe-ac93-ca20a3642b11" + } + ], + "tempVars": [ + { + "tempVar": ":dbs:", + "values": [ + { + "value": "_internal", + "type": "database", + "selected": true + } + ], + "id": "792eda0d-2bb2-4de6-a86f-1f652889b044", + "type": "databases", + "label": "", + "query": { + "influxql": "SHOW DATABASES", + "measurement": "", + "tagKey": "", + "fieldKey": "" + }, + "links": { + "self": "/chronograf/v1/dashboards/1/templates/792eda0d-2bb2-4de6-a86f-1f652889b044" + } + }, + { + "id": "dashtime", + "tempVar": ":dashboardTime:", + "type": "constant", + "values": [ + { + "value": "now() - 15m", + "type": "constant", + "selected": true + } + ] + }, + { + "id": "upperdashtime", + "tempVar": ":upperDashboardTime:", + "type": "constant", + "values": [ + { + "value": "now()", + "type": "constant", + "selected": true + } + ] + }, + { + "id": "interval", + "type": "constant", + "tempVar": ":interval:", + "resolution": 1000, + "reportingInterval": 10000000000, + "values": [] + } + ] + }`))), + want: `{"queries":[{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","query":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","queryConfig":{"id":"82b60d37-251e-4afe-ac93-ca20a3642b11","database":"_internal","measurement":"httpd","retentionPolicy":"monitor","fields":[{"value":"pingReq","type":"field","alias":""}],"tags":{},"groupBy":{"time":"","tags":[]},"areTagsAccepted":false,"rawText":"SELECT \"pingReq\" FROM :dbs:.\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","range":{"upper":"","lower":"now() - 1m"}},"queryAST":{"condition":{"expr":"binary","op":"\u003e","lhs":{"expr":"reference","val":"time"},"rhs":{"expr":"binary","op":"-","lhs":{"expr":"call","name":"now"},"rhs":{"expr":"literal","val":"1m","type":"duration"}}},"fields":[{"column":{"expr":"reference","val":"pingReq"}}],"sources":[{"database":"_internal","retentionPolicy":"monitor","name":"httpd","type":"measurement"}]},"queryTemplated":"SELECT \"pingReq\" FROM \"_internal\".\"monitor\".\"httpd\" WHERE time \u003e now() - 1m","tempVars":[{"tempVar":":dbs:","values":[{"value":"_internal","type":"database","selected":true}]},{"tempVar":":dashboardTime:","values":[{"value":"now() - 15m","type":"constant","selected":true}]},{"tempVar":":upperDashboardTime:","values":[{"value":"now()","type":"constant","selected":true}]},{"tempVar":":interval:","duration":60000000000,"resolution":1000,"reportingInterval":10000000000}]}]} +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.r = tt.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + s := &Service{ + SourcesStore: tt.SourcesStore, + Logger: &mocks.TestLogger{}, + } + s.Queries(tt.w, tt.r) + got := tt.w.Body.String() + if got != tt.want { + t.Errorf("got:\n%s\nwant:\n%s\n", got, tt.want) + } + }) + } +} From 38f70bff3e6069d8830f001da2841095509e19de Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Thu, 2 Nov 2017 18:01:35 -0500 Subject: [PATCH 24/38] Update CHANGELOG to mention fixing template variable in dashboard query building --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ed7653db..9370881df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 1. [#2158](https://github.com/influxdata/chronograf/pull/2158): Fix 'Cannot connect to source' false error flag on Dashboard page 1. [#2167](https://github.com/influxdata/chronograf/pull/2167): Add fractions of seconds to time field in csv export 1. [#1077](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requiring Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page. +1. [#2222](https://github.com/influxdata/chronograf/pull/2222): Fix template variables in dashboard query building. ### Features ### UI Improvements From 39096e6c3db39bfbd25dad6827626f73d0d89708 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Fri, 3 Nov 2017 19:33:16 -0500 Subject: [PATCH 25/38] Add cleanup step to js dev builds to remove unused files --- ui/package.json | 1 + ui/webpack/devConfig.js | 66 +++++++++++++++++++++++++++++++---------- ui/yarn.lock | 4 +++ 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/ui/package.json b/ui/package.json index 6bdd366142..6355c75ad7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -77,6 +77,7 @@ "mocha-loader": "^0.7.1", "mustache": "^2.2.1", "node-sass": "^4.5.3", + "on-build-webpack": "^0.1.0", "postcss-browser-reporter": "^0.4.0", "postcss-calc": "^5.2.0", "postcss-loader": "^0.8.0", diff --git a/ui/webpack/devConfig.js b/ui/webpack/devConfig.js index acef0ed035..5bba034e6d 100644 --- a/ui/webpack/devConfig.js +++ b/ui/webpack/devConfig.js @@ -1,11 +1,16 @@ -var webpack = require('webpack'); -var path = require('path'); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); -var HtmlWebpackPlugin = require("html-webpack-plugin"); -var package = require('../package.json'); -var dependencies = package.dependencies; +var webpack = require('webpack') +var path = require('path') +var ExtractTextPlugin = require('extract-text-webpack-plugin') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var package = require('../package.json') +const WebpackOnBuildPlugin = require('on-build-webpack') +const fs = require('fs') +var dependencies = package.dependencies + +const buildDir = path.resolve(__dirname, '../build') module.exports = { + watch: true, devtool: 'source-map', entry: { app: path.resolve(__dirname, '..', 'src', 'index.js'), @@ -48,15 +53,21 @@ module.exports = { }, { test: /\.scss$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader!resolve-url!sass?sourceMap'), + loader: ExtractTextPlugin.extract( + 'style-loader', + 'css-loader!sass-loader!resolve-url!sass?sourceMap' + ), }, { test: /\.css$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'), + loader: ExtractTextPlugin.extract( + 'style-loader', + 'css-loader!postcss-loader' + ), }, { - test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - loader : 'file', + test: /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + loader: 'file', }, { test: /\.js$/, @@ -70,7 +81,7 @@ module.exports = { ], }, sassLoader: { - includePaths: [path.resolve(__dirname, "node_modules")], + includePaths: [path.resolve(__dirname, 'node_modules')], }, eslint: { failOnWarning: false, @@ -78,10 +89,10 @@ module.exports = { }, plugins: [ new webpack.ProvidePlugin({ - $: "jquery", - jQuery: "jquery", + $: 'jquery', + jQuery: 'jquery', }), - new ExtractTextPlugin("chronograf.css"), + new ExtractTextPlugin('chronograf.css'), new HtmlWebpackPlugin({ template: path.resolve(__dirname, '..', 'src', 'index.template.html'), inject: 'body', @@ -93,7 +104,32 @@ module.exports = { new webpack.DefinePlugin({ VERSION: JSON.stringify(require('../package.json').version), }), + new WebpackOnBuildPlugin(function(stats) { + const newlyCreatedAssets = stats.compilation.assets + + const unlinked = [] + fs.readdir(path.resolve(buildDir), (err, files) => { + files.forEach(file => { + if (!newlyCreatedAssets[file]) { + console.log('Removed ', file) + const del = path.resolve(buildDir + file) + fs.stat(del, function(err, stat) { + if (err == null) { + try { + fs.unlink(path.resolve(buildDir + file)) + console.log('Removed ', file) + unlinked.push(file) + } catch (e) {} + } + }) + } + }) + if (unlinked.length > 0) { + console.log('Removed old assets: ', unlinked) + } + }) + }), ], postcss: require('./postcss'), target: 'web', -}; +} diff --git a/ui/yarn.lock b/ui/yarn.lock index 373b561452..966c0876e8 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5134,6 +5134,10 @@ object.values@^1.0.3: function-bind "^1.1.0" has "^1.0.1" +on-build-webpack@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/on-build-webpack/-/on-build-webpack-0.1.0.tgz#a287c0e17766e6141926e5f2cbb0d8bb53b76814" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" From 1bcf74e547af59f55da0add7a49bc3f80647f6d0 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Sat, 4 Nov 2017 14:07:39 -0500 Subject: [PATCH 26/38] Add hot module reload to dev builds (make run-hmr) --- Makefile | 49 +++++++++++++++++++++++++++++++++-------- ui/package.json | 1 + ui/webpack/devConfig.js | 21 +++++++++++++++++- 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index a4de270cc2..29cc23061a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: assets dep clean test gotest gotestrace jstest run run-dev ctags continuous +.PHONY: assets dep clean test gotest gotestrace jstest run run-dev run-hmr ctags VERSION ?= $(shell git describe --always --tags) COMMIT ?= $(shell git rev-parse --short=8 HEAD) @@ -23,14 +23,42 @@ ${BINARY}: $(SOURCES) .bindata .jsdep .godep go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go define CHRONOGIRAFFE - ._ o o - \_`-)|_ - ,"" _\_ - ," ## | 0 0. - ," ## ,-\__ `. - ," / `--._;) - "HAI, I'm Chronogiraffe. Let's be friends!" - ," ## / -," ## / + tLf iCf. + .CCC. tCC: + CGG; CGG: +tG0Gt: GGGGGGGGGGGGGGGG1 .,:, +LG1,,:1CC: .GGL;iLC1iii1LCi;GG1 .1GCL1iGG1 + LG1:::;i1CGGt;;;;;;L0t;;;;;;GGGC1;;::,iGC + ,ii:. 1GG1iiii;;tfiC;;;;;;;GGCfCGCGGC, + fGCiiiiGi1Lt;;iCLL,i;;;CGt + fGG11iiii1C1iiiiiGt1;;;;;CGf + .GGLLL1i1CitfiiL1iCi;;iLCGGt + .CGL11LGCCCCCCCLLCGG1;1GG; + CGL1tf1111iiiiiiL1ifGG, + LGCff1fCt1tCfiiCiCGC + LGGf111111111iCGGt + fGGGGGGGGGGGGGGi + ifii111111itL + ;f1i11111iitf + ;f1iiiiiii1tf + :fi111iii11tf + :fi111ii1i1tf + :f111111ii1tt + ,L111111ii1tt + .Li1111i1111CCCCCCCCCCCCCCLt; + L111ii11111ittttt1tttttittti1fC; + f1111ii111i1ttttt1;iii1ittt1ttttCt. + tt11ii111tti1ttt1tt1;11;;;;iitttifCCCL, + 11i1i11ttttti;1t1;;;ttt1;;ii;itti;L,;CCL + ;f;;;;1tttti;;ttti;;;;;;;;;;;1tt1ifi .CCi + ,L;itti;;;it;;;;;tt1;;;t1;;;;;;ii;t; :CC, + L;;;;iti;;;;;;;;;;;;;;;;;;;;;;;i;L, ;CC. + ti;;;iLLfffi;;;;;ittt11i;;;;;;;;;L tCCfff; + it;;;;;;L,ti;;;;;1Ltttft1t;;;;;;1t ;CCCL; + :f;;;;;;L.ti;;;;;tftttf1,f;;;;;;f: ;CC1: + .L;;;;;;L.t1;;;;;tt111fi,f;;;;;;L. + 1Li;;iL1 :Ci;;;tL1i1fC, Lt;;;;Li + .;tt; ifLt:;fLf; ;LCCt, endef export CHRONOGIRAFFE chronogiraffe: ${BINARY} @@ -106,6 +134,9 @@ run: ${BINARY} run-dev: chronogiraffe ./chronograf -d --log-level=debug +run-hmr: + cd ui && npm run start:hmr + clean: if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi cd ui && yarn run clean diff --git a/ui/package.json b/ui/package.json index 6355c75ad7..13dc395315 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "build": "yarn run clean && env NODE_ENV=production webpack --optimize-minimize --config ./webpack/prodConfig.js", "build:dev": "webpack --config ./webpack/devConfig.js", "start": "webpack --watch --config ./webpack/devConfig.js", + "start:hmr": "webpack-dev-server --open --config ./webpack/devConfig.js", "lint": "esw src/", "test": "karma start", "test:integration": "nightwatch tests --skip", diff --git a/ui/webpack/devConfig.js b/ui/webpack/devConfig.js index 5bba034e6d..e5103141e5 100644 --- a/ui/webpack/devConfig.js +++ b/ui/webpack/devConfig.js @@ -19,7 +19,7 @@ module.exports = { output: { publicPath: '/', path: path.resolve(__dirname, '../build'), - filename: '[name].[chunkhash].dev.js', + filename: '[name].[hash].dev.js', }, resolve: { alias: { @@ -88,6 +88,7 @@ module.exports = { failOnError: false, }, plugins: [ + new webpack.HotModuleReplacementPlugin(), new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', @@ -132,4 +133,22 @@ module.exports = { ], postcss: require('./postcss'), target: 'web', + devServer: { + hot: true, + historyApiFallback: true, + clientLogLevel: "info", + stats: { colors: true }, + contentBase: 'build', + quiet: false, + watchOptions: { + aggregateTimeout: 300, + poll: 1000 + }, + proxy: { + '/chronograf/v1': { + target: 'http://localhost:8888', + secure: false, + }, + }, + }, } From 02762a95e5434f7fcc877a736a615a7988c82a00 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Sat, 4 Nov 2017 20:19:08 -0500 Subject: [PATCH 27/38] Update queries endpoint comment --- server/queries.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/queries.go b/server/queries.go index 7966be49ec..16b2c5a7fa 100644 --- a/server/queries.go +++ b/server/queries.go @@ -41,7 +41,7 @@ type QueriesResponse struct { Queries []QueryResponse `json:"queries"` } -// Queries parses InfluxQL and returns mostly importantly a structured QueryConfig +// Queries analyzes InfluxQL to produce front-end friendly QueryConfig func (s *Service) Queries(w http.ResponseWriter, r *http.Request) { srcID, err := paramID("id", r) if err != nil { From f4fa08e36b363cc264d7118fc38828c3c6783b42 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Tue, 7 Nov 2017 11:42:40 -0600 Subject: [PATCH 28/38] Fix kapacitor task panic where error condition was not checked --- kapacitor/client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/kapacitor/client.go b/kapacitor/client.go index da6eaa8a57..146eac612c 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -319,6 +319,9 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR } else { opt, err = c.updateFromTick(rule) } + if err != nil { + return nil, err + } task, err := kapa.UpdateTask(client.Link{Href: href}, *opt) if err != nil { From b1ad6443e5f1827e6507013ae91996a16a1ec219 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Tue, 7 Nov 2017 15:59:41 -0600 Subject: [PATCH 29/38] Fix several kapacitor validation failures --- Gopkg.lock | 4 +- kapacitor/client_test.go | 263 +++++++++++++++++++++-- kapacitor/operators.go | 4 +- kapacitor/tickscripts.go | 4 +- kapacitor/triggers.go | 7 +- kapacitor/vars.go | 50 ++++- ui/src/kapacitor/components/Relative.js | 4 +- ui/src/kapacitor/components/Threshold.js | 4 +- ui/src/kapacitor/constants/index.js | 11 +- 9 files changed, 321 insertions(+), 30 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index d8021c0107..e23664920c 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -49,7 +49,7 @@ [[projects]] name = "github.com/google/go-cmp" - packages = ["cmp"] + packages = ["cmp","cmp/cmpopts"] revision = "79b2d888f100ec053545168aa94bcfb322e8bfc8" [[projects]] @@ -140,6 +140,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f34fb88755292baba8b52c14bf5b9a028daff96a763368a7cf1de90004d33695" + inputs-digest = "85a5451fc9e0596e486a676204eb2de0b12900522341ee0804cf9ec86fb2765e" solver-name = "gps-cdcl" solver-version = 1 diff --git a/kapacitor/client_test.go b/kapacitor/client_test.go index 9ea8903f56..c72ebfdaee 100644 --- a/kapacitor/client_test.go +++ b/kapacitor/client_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/influxdata/chronograf" client "github.com/influxdata/kapacitor/client/v1" ) @@ -945,10 +946,22 @@ func TestClient_Update(t *testing.T) { ctx: context.Background(), href: "/kapacitor/v1/tasks/howdy", rule: chronograf.AlertRule{ - ID: "howdy", + ID: "howdy", + Name: "myname", Query: &chronograf.QueryConfig{ Database: "db", RetentionPolicy: "rp", + Measurement: "meas", + Fields: []chronograf.Field{ + { + Type: "field", + Value: "usage_user", + }, + }, + }, + Trigger: "threshold", + TriggerValues: chronograf.TriggerValues{ + Operator: greaterThan, }, }, }, @@ -1009,10 +1022,22 @@ func TestClient_Update(t *testing.T) { ctx: context.Background(), href: "/kapacitor/v1/tasks/howdy", rule: chronograf.AlertRule{ - ID: "howdy", + ID: "howdy", + Name: "myname", Query: &chronograf.QueryConfig{ Database: "db", RetentionPolicy: "rp", + Measurement: "meas", + Fields: []chronograf.Field{ + { + Type: "field", + Value: "usage_user", + }, + }, + }, + Trigger: "threshold", + TriggerValues: chronograf.TriggerValues{ + Operator: greaterThan, }, }, }, @@ -1061,6 +1086,135 @@ func TestClient_Update(t *testing.T) { }, wantStatus: client.Disabled, }, + { + name: "error because relative cannot have inside range", + wantErr: true, + fields: fields{ + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/error", + rule: chronograf.AlertRule{ + ID: "error", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + Fields: []chronograf.Field{ + { + Value: "usage_user", + Type: "field", + }, + }, + }, + Trigger: Relative, + TriggerValues: chronograf.TriggerValues{ + Operator: InsideRange, + }, + }, + }, + }, + { + name: "error because rule has an unknown trigger mechanism", + wantErr: true, + fields: fields{ + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/error", + rule: chronograf.AlertRule{ + ID: "error", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + }, + { + name: "error because query has no fields", + wantErr: true, + fields: fields{ + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/error", + rule: chronograf.AlertRule{ + ID: "error", + Trigger: Threshold, + TriggerValues: chronograf.TriggerValues{ + Period: "1d", + }, + Name: "myname", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + Measurement: "meas", + }, + }, + }, + }, + { + name: "error because alert has no name", + wantErr: true, + fields: fields{ + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/error", + rule: chronograf.AlertRule{ + ID: "error", + Trigger: Deadman, + TriggerValues: chronograf.TriggerValues{ + Period: "1d", + }, + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + Measurement: "meas", + }, + }, + }, + }, + { + name: "error because alert period cannot be an empty string in deadman alert", + wantErr: true, + fields: fields{ + kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/error", + rule: chronograf.AlertRule{ + ID: "error", + Name: "myname", + Trigger: Deadman, + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + Measurement: "meas", + }, + }, + }, + }, } for _, tt := range tests { kapa.ResTask = tt.resTask @@ -1079,11 +1233,17 @@ func TestClient_Update(t *testing.T) { t.Errorf("Client.Update() error = %v, wantErr %v", err, tt.wantErr) return } + if tt.wantErr { + return + } if !cmp.Equal(got, tt.want) { t.Errorf("%q. Client.Update() = -got/+want %s", tt.name, cmp.Diff(got, tt.want)) } - if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) { - t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions) + var cmpOptions = cmp.Options{ + cmpopts.IgnoreFields(client.UpdateTaskOptions{}, "TICKscript"), + } + if !cmp.Equal(kapa.UpdateTaskOptions, tt.updateTaskOptions, cmpOptions...) { + t.Errorf("Client.Update() = %s", cmp.Diff(got, tt.updateTaskOptions, cmpOptions...)) } if tt.wantStatus != kapa.LastStatus { t.Errorf("Client.Update() = %v, want %v", kapa.LastStatus, tt.wantStatus) @@ -1130,10 +1290,16 @@ func TestClient_Create(t *testing.T) { args: args{ ctx: context.Background(), rule: chronograf.AlertRule{ - ID: "howdy", + ID: "howdy", + Name: "mynames", Query: &chronograf.QueryConfig{ Database: "db", RetentionPolicy: "rp", + Measurement: "meas", + }, + Trigger: Deadman, + TriggerValues: chronograf.TriggerValues{ + Period: "1d", }, }, }, @@ -1152,10 +1318,79 @@ func TestClient_Create(t *testing.T) { }, }, createTaskOptions: &client.CreateTaskOptions{ - TICKscript: "", - ID: "chronograf-v1-howdy", - Type: client.StreamTask, - Status: client.Enabled, + TICKscript: `var db = 'db' + +var rp = 'rp' + +var measurement = 'meas' + +var groupBy = [] + +var whereFilter = lambda: TRUE + +var period = 1d + +var name = 'mynames' + +var idVar = name + ':{{.Group}}' + +var message = '' + +var idTag = 'alertID' + +var levelTag = 'level' + +var messageField = 'message' + +var durationField = 'duration' + +var outputDB = 'chronograf' + +var outputRP = 'autogen' + +var outputMeasurement = 'alerts' + +var triggerType = 'deadman' + +var threshold = 0.0 + +var data = stream + |from() + .database(db) + .retentionPolicy(rp) + .measurement(measurement) + .groupBy(groupBy) + .where(whereFilter) + +var trigger = data + |deadman(threshold, period) + .stateChangesOnly() + .message(message) + .id(idVar) + .idTag(idTag) + .levelTag(levelTag) + .messageField(messageField) + .durationField(durationField) + +trigger + |eval(lambda: "emitted") + .as('value') + .keep('value', messageField, durationField) + |influxDBOut() + .create() + .database(outputDB) + .retentionPolicy(outputRP) + .measurement(outputMeasurement) + .tag('alertName', name) + .tag('triggerType', triggerType) + +trigger + |httpOut('output') +`, + + ID: "chronograf-v1-howdy", + Type: client.StreamTask, + Status: client.Enabled, DBRPs: []client.DBRP{ { Database: "db", @@ -1205,10 +1440,9 @@ func TestClient_Create(t *testing.T) { }, resError: fmt.Errorf("error"), createTaskOptions: &client.CreateTaskOptions{ - TICKscript: "", - ID: "chronograf-v1-howdy", - Type: client.StreamTask, - Status: client.Enabled, + ID: "chronograf-v1-howdy", + Type: client.StreamTask, + Status: client.Enabled, DBRPs: []client.DBRP{ { Database: "db", @@ -1236,6 +1470,9 @@ func TestClient_Create(t *testing.T) { t.Errorf("Client.Create() error = %v, wantErr %v", err, tt.wantErr) return } + if tt.wantErr { + return + } if !cmp.Equal(got, tt.want) { t.Errorf("%q. Client.Create() = -got/+want %s", tt.name, cmp.Diff(got, tt.want)) } diff --git a/kapacitor/operators.go b/kapacitor/operators.go index 6458f88d84..8c319e4f86 100644 --- a/kapacitor/operators.go +++ b/kapacitor/operators.go @@ -1,6 +1,8 @@ package kapacitor -import "fmt" +import ( + "fmt" +) const ( greaterThan = "greater than" diff --git a/kapacitor/tickscripts.go b/kapacitor/tickscripts.go index 4deefdf0ac..8b4259a042 100644 --- a/kapacitor/tickscripts.go +++ b/kapacitor/tickscripts.go @@ -15,11 +15,11 @@ type Alert struct{} func (a *Alert) Generate(rule chronograf.AlertRule) (chronograf.TICKScript, error) { vars, err := Vars(rule) if err != nil { - return "", nil + return "", err } data, err := Data(rule) if err != nil { - return "", nil + return "", err } trigger, err := Trigger(rule) if err != nil { diff --git a/kapacitor/triggers.go b/kapacitor/triggers.go index a5417f0916..da92832ebe 100644 --- a/kapacitor/triggers.go +++ b/kapacitor/triggers.go @@ -1,7 +1,10 @@ package kapacitor -import "github.com/influxdata/chronograf" -import "fmt" +import ( + "fmt" + + "github.com/influxdata/chronograf" +) const ( // Deadman triggers when data is missing for a period of time diff --git a/kapacitor/vars.go b/kapacitor/vars.go index 99c4d5a6a0..f89b328dc7 100644 --- a/kapacitor/vars.go +++ b/kapacitor/vars.go @@ -76,7 +76,37 @@ func Vars(rule chronograf.AlertRule) (string, error) { } } +type NotEmpty struct { + Err error +} + +func (n *NotEmpty) Valid(name, s string) error { + if n.Err != nil { + return n.Err + + } + if s == "" { + n.Err = fmt.Errorf("%s cannot be an empty string", name) + } + return n.Err +} + func commonVars(rule chronograf.AlertRule) (string, error) { + n := new(NotEmpty) + n.Valid("database", rule.Query.Database) + n.Valid("retention policy", rule.Query.RetentionPolicy) + n.Valid("measurement", rule.Query.Measurement) + n.Valid("alert name", rule.Name) + n.Valid("trigger type", rule.Trigger) + if n.Err != nil { + return "", n.Err + } + + wind, err := window(rule) + if err != nil { + return "", err + } + common := ` var db = '%s' var rp = '%s' @@ -104,7 +134,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) { rule.Query.Measurement, groupBy(rule.Query), whereFilter(rule.Query), - window(rule), + wind, rule.Name, rule.Message, IDTag, @@ -127,17 +157,27 @@ func commonVars(rule chronograf.AlertRule) (string, error) { // window is only used if deadman or threshold/relative with aggregate. Will return empty // if no period. -func window(rule chronograf.AlertRule) string { +func window(rule chronograf.AlertRule) (string, error) { if rule.Trigger == Deadman { - return fmt.Sprintf("var period = %s", rule.TriggerValues.Period) + if rule.TriggerValues.Period == "" { + return "", fmt.Errorf("period cannot be an empty string in deadman alert") + } + return fmt.Sprintf("var period = %s", rule.TriggerValues.Period), nil + } // Period only makes sense if the field has a been grouped via a time duration. for _, field := range rule.Query.Fields { if field.Type == "func" { - return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every) + n := new(NotEmpty) + n.Valid("group by time", rule.Query.GroupBy.Time) + n.Valid("every", rule.Every) + if n.Err != nil { + return "", n.Err + } + return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every), nil } } - return "" + return "", nil } func groupBy(q *chronograf.QueryConfig) string { diff --git a/ui/src/kapacitor/components/Relative.js b/ui/src/kapacitor/components/Relative.js index 04ca3886d3..91709940a5 100644 --- a/ui/src/kapacitor/components/Relative.js +++ b/ui/src/kapacitor/components/Relative.js @@ -1,11 +1,11 @@ import React, {PropTypes} from 'react' -import {CHANGES, OPERATORS, SHIFTS} from 'src/kapacitor/constants' +import {CHANGES, RELATIVE_OPERATORS, SHIFTS} from 'src/kapacitor/constants' import Dropdown from 'shared/components/Dropdown' const mapToItems = (arr, type) => arr.map(text => ({text, type})) const changes = mapToItems(CHANGES, 'change') const shifts = mapToItems(SHIFTS, 'shift') -const operators = mapToItems(OPERATORS, 'operator') +const operators = mapToItems(RELATIVE_OPERATORS, 'operator') const Relative = ({ onRuleTypeInputChange, diff --git a/ui/src/kapacitor/components/Threshold.js b/ui/src/kapacitor/components/Threshold.js index 7574220d83..b43cfbb89c 100644 --- a/ui/src/kapacitor/components/Threshold.js +++ b/ui/src/kapacitor/components/Threshold.js @@ -1,10 +1,10 @@ import React, {PropTypes} from 'react' -import {OPERATORS} from 'src/kapacitor/constants' +import {THRESHOLD_OPERATORS} from 'src/kapacitor/constants' import Dropdown from 'shared/components/Dropdown' import _ from 'lodash' const mapToItems = (arr, type) => arr.map(text => ({text, type})) -const operators = mapToItems(OPERATORS, 'operator') +const operators = mapToItems(THRESHOLD_OPERATORS, 'operator') const noopSubmit = e => e.preventDefault() const getField = ({fields}) => { const alias = _.get(fields, ['0', 'alias'], false) diff --git a/ui/src/kapacitor/constants/index.js b/ui/src/kapacitor/constants/index.js index 181543e683..e211bbdaf6 100644 --- a/ui/src/kapacitor/constants/index.js +++ b/ui/src/kapacitor/constants/index.js @@ -31,7 +31,7 @@ export const OUTSIDE_RANGE = 'outside range' export const EQUAL_TO_OR_GREATER_THAN = 'equal to or greater' export const EQUAL_TO_OR_LESS_THAN = 'equal to or less than' -export const OPERATORS = [ +export const THRESHOLD_OPERATORS = [ GREATER_THAN, EQUAL_TO_OR_GREATER_THAN, EQUAL_TO_OR_LESS_THAN, @@ -42,6 +42,15 @@ export const OPERATORS = [ OUTSIDE_RANGE, ] +export const RELATIVE_OPERATORS = [ + GREATER_THAN, + EQUAL_TO_OR_GREATER_THAN, + EQUAL_TO_OR_LESS_THAN, + LESS_THAN, + EQUAL_TO, + NOT_EQUAL_TO, +] + // export const RELATIONS = ['once', 'more than ', 'less than']; export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h'] export const CHANGES = ['change', '% change'] From 943b6129c3c202c7b4212f50d4213cdd0b7bc254 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Tue, 7 Nov 2017 16:07:21 -0600 Subject: [PATCH 30/38] Fix kapacitor variables to escape strings --- kapacitor/client_test.go | 4 ++-- kapacitor/vars.go | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/kapacitor/client_test.go b/kapacitor/client_test.go index c72ebfdaee..ee82e2d236 100644 --- a/kapacitor/client_test.go +++ b/kapacitor/client_test.go @@ -1291,7 +1291,7 @@ func TestClient_Create(t *testing.T) { ctx: context.Background(), rule: chronograf.AlertRule{ ID: "howdy", - Name: "mynames", + Name: "myname's", Query: &chronograf.QueryConfig{ Database: "db", RetentionPolicy: "rp", @@ -1330,7 +1330,7 @@ var whereFilter = lambda: TRUE var period = 1d -var name = 'mynames' +var name = 'myname\'s' var idVar = name + ':{{.Group}}' diff --git a/kapacitor/vars.go b/kapacitor/vars.go index f89b328dc7..4ef867a2d7 100644 --- a/kapacitor/vars.go +++ b/kapacitor/vars.go @@ -91,6 +91,10 @@ func (n *NotEmpty) Valid(name, s string) error { return n.Err } +func Escape(str string) string { + return strings.Replace(str, "'", `\'`, -1) +} + func commonVars(rule chronograf.AlertRule) (string, error) { n := new(NotEmpty) n.Valid("database", rule.Query.Database) @@ -129,14 +133,14 @@ func commonVars(rule chronograf.AlertRule) (string, error) { var triggerType = '%s' ` res := fmt.Sprintf(common, - rule.Query.Database, - rule.Query.RetentionPolicy, - rule.Query.Measurement, + Escape(rule.Query.Database), + Escape(rule.Query.RetentionPolicy), + Escape(rule.Query.Measurement), groupBy(rule.Query), whereFilter(rule.Query), wind, - rule.Name, - rule.Message, + Escape(rule.Name), + Escape(rule.Message), IDTag, LevelTag, MessageField, From 4b322052b34974af9fc6e8b28142d67e087ad851 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Tue, 7 Nov 2017 16:10:31 -0600 Subject: [PATCH 31/38] Update CHANGELOG to mention fixing kapacitor panics --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9370881df2..724bb3c243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ 1. [#2167](https://github.com/influxdata/chronograf/pull/2167): Add fractions of seconds to time field in csv export 1. [#1077](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requiring Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page. 1. [#2222](https://github.com/influxdata/chronograf/pull/2222): Fix template variables in dashboard query building. +1. [#2291](https://github.com/influxdata/chronograf/pull/2291): Fix several kapacitor alert creation panics. ### Features ### UI Improvements From dd505ac58743f26ccbf6a90f7843cac32627f223 Mon Sep 17 00:00:00 2001 From: Luke Bigum Date: Tue, 7 Nov 2017 17:52:45 +0000 Subject: [PATCH 32/38] source a /etc/defaults script so you can specify extra chronograf options at service start --- CHANGELOG.md | 2 ++ etc/scripts/chronograf.service | 2 +- etc/scripts/init.sh | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 724bb3c243..61bfde3548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ 1. [#1077](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requiring Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page. 1. [#2222](https://github.com/influxdata/chronograf/pull/2222): Fix template variables in dashboard query building. 1. [#2291](https://github.com/influxdata/chronograf/pull/2291): Fix several kapacitor alert creation panics. +1. [#2292](https://github.com/influxdata/chronograf/pull/2292): Source extra command line options from defaults file + ### Features ### UI Improvements diff --git a/etc/scripts/chronograf.service b/etc/scripts/chronograf.service index dd84b1f2b4..ba5c973b1d 100644 --- a/etc/scripts/chronograf.service +++ b/etc/scripts/chronograf.service @@ -9,7 +9,7 @@ After=network-online.target User=chronograf Group=chronograf EnvironmentFile=-/etc/default/chronograf -ExecStart=/usr/bin/chronograf --host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned +ExecStart=/usr/bin/chronograf --host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned $CHRONOGRAF_OPTS KillMode=control-group Restart=on-failure diff --git a/etc/scripts/init.sh b/etc/scripts/init.sh index 9898a9e51e..739a3d93c1 100755 --- a/etc/scripts/init.sh +++ b/etc/scripts/init.sh @@ -13,7 +13,8 @@ # Script to execute when starting SCRIPT="/usr/bin/chronograf" # Options to pass to the script on startup -SCRIPT_OPTS="--host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned" +. /etc/default/chronograf +SCRIPT_OPTS="--host 0.0.0.0 --port 8888 -b /var/lib/chronograf/chronograf-v1.db -c /usr/share/chronograf/canned ${CHRONOGRAF_OPTS}" # User to run the process under RUNAS=chronograf From e4b08fd8ec50cc834f0431914fb8bbb08a99230e Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Wed, 8 Nov 2017 06:58:40 -0600 Subject: [PATCH 33/38] Add shadow-utils to release RPM generation --- etc/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/build.py b/etc/build.py index 3ffc229d23..076e8b6769 100755 --- a/etc/build.py +++ b/etc/build.py @@ -674,7 +674,7 @@ def package(build_output, pkg_name, version, nightly=False, iteration=1, static= package_build_root, current_location) if package_type == "rpm": - fpm_command += "--depends coreutils" + fpm_command += "--depends coreutils --depends shadow-utils" # TODO: Check for changelog # elif package_type == "deb": # fpm_command += "--deb-changelog {} ".format(os.path.join(os.getcwd(), "CHANGELOG.md")) From 7b00215a7f421812ca73d2094fa65d12d1047d68 Mon Sep 17 00:00:00 2001 From: Chris Goller Date: Wed, 8 Nov 2017 07:01:02 -0600 Subject: [PATCH 34/38] Update CHANGELOG to mention shadow-utils in RPM release packages --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 724bb3c243..399f39aff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 1. [#1077](https://github.com/influxdata/chronograf/pull/2087): Fix Chronograf requiring Telegraf's CPU and system plugins to ensure that all Apps appear on the HOST LIST page. 1. [#2222](https://github.com/influxdata/chronograf/pull/2222): Fix template variables in dashboard query building. 1. [#2291](https://github.com/influxdata/chronograf/pull/2291): Fix several kapacitor alert creation panics. +1. [#2303](https://github.com/influxdata/chronograf/pull/2303): Add shadow-utils to RPM release packages ### Features ### UI Improvements From 50a0ae90d5dbe16dd66cb8d82600cb8b15831d48 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 9 Nov 2017 10:01:21 -0800 Subject: [PATCH 35/38] Remove console spam --- ui/webpack/devConfig.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ui/webpack/devConfig.js b/ui/webpack/devConfig.js index e5103141e5..a991002a35 100644 --- a/ui/webpack/devConfig.js +++ b/ui/webpack/devConfig.js @@ -112,22 +112,17 @@ module.exports = { fs.readdir(path.resolve(buildDir), (err, files) => { files.forEach(file => { if (!newlyCreatedAssets[file]) { - console.log('Removed ', file) const del = path.resolve(buildDir + file) fs.stat(del, function(err, stat) { if (err == null) { try { fs.unlink(path.resolve(buildDir + file)) - console.log('Removed ', file) unlinked.push(file) } catch (e) {} } }) } }) - if (unlinked.length > 0) { - console.log('Removed old assets: ', unlinked) - } }) }), ], @@ -136,13 +131,13 @@ module.exports = { devServer: { hot: true, historyApiFallback: true, - clientLogLevel: "info", - stats: { colors: true }, + clientLogLevel: 'info', + stats: {colors: true}, contentBase: 'build', quiet: false, watchOptions: { aggregateTimeout: 300, - poll: 1000 + poll: 1000, }, proxy: { '/chronograf/v1': { From 5764511d8f09f1662958e9b6b817473a23dc6ec4 Mon Sep 17 00:00:00 2001 From: Luke Morris Date: Thu, 9 Nov 2017 18:38:09 -0800 Subject: [PATCH 36/38] DE tab names include measurement and all tag values --- ui/src/data_explorer/components/Table.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index eefe3bec6b..e9bdd6fe6e 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -88,7 +88,14 @@ class ChronoTable extends Component { ) } - makeTabName = ({name, tags}) => (tags ? `${name}.${tags[name]}` : name) + makeTabName = ({name, tags}) => { + if (!tags) { + return name + } + const tagKeys = Object.keys(tags).sort() + const tagValues = tagKeys.map(key => tags[key]).join('.') + return `${name}.${tagValues}` + } render() { const {containerWidth, height, query} = this.props @@ -135,9 +142,13 @@ class ChronoTable extends Component {
: ({...s, text: s.name, index}))} + items={series.map((s, index) => ({ + ...s, + text: this.makeTabName(s), + index, + }))} onChoose={this.handleClickDropdown} - selected={series[activeSeriesIndex].name} + selected={this.makeTabName(series[activeSeriesIndex])} buttonSize="btn-xs" />}
From 2563dadfcd2ef47b470281485a45b06c95f19f26 Mon Sep 17 00:00:00 2001 From: Luke Morris Date: Thu, 9 Nov 2017 18:42:40 -0800 Subject: [PATCH 37/38] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b43ff39f..191ae6f283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 1. [#2291](https://github.com/influxdata/chronograf/pull/2291): Fix several kapacitor alert creation panics. 1. [#2303](https://github.com/influxdata/chronograf/pull/2303): Add shadow-utils to RPM release packages 1. [#2292](https://github.com/influxdata/chronograf/pull/2292): Source extra command line options from defaults file +1. [#2329](https://github.com/influxdata/chronograf/pull/2329): Include tag values alongside measurement name in Data Explorer result tabs ### Features ### UI Improvements From 78705185f4f91f29091067356d4d57c4ad3778c1 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Thu, 16 Nov 2017 12:02:45 -0800 Subject: [PATCH 38/38] Add mock.Store to queries test Signed-off-by: Jared Scheib --- server/queries_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/queries_test.go b/server/queries_test.go index 3e409779fe..bd107775d9 100644 --- a/server/queries_test.go +++ b/server/queries_test.go @@ -174,8 +174,10 @@ func TestService_Queries(t *testing.T) { }, })) s := &Service{ - SourcesStore: tt.SourcesStore, - Logger: &mocks.TestLogger{}, + Store: &mocks.Store{ + SourcesStore: tt.SourcesStore, + }, + Logger: &mocks.TestLogger{}, } s.Queries(tt.w, tt.r) got := tt.w.Body.String()