From 5f5a8c2ee5eebccc86c37f33a861f790e63d7501 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 3 Apr 2018 15:58:33 -0700 Subject: [PATCH] Introduce /health endpoint --- server/mux.go | 1 + server/sources.go | 34 ++++++++++++++++++++++++ server/swagger.json | 41 ++++++++++++++++++++++++++++- ui/src/CheckSources.tsx | 30 ++++++--------------- ui/src/shared/copy/notifications.js | 2 +- ui/src/sources/apis/index.ts | 10 +++++++ 6 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 ui/src/sources/apis/index.ts diff --git a/server/mux.go b/server/mux.go index 11730293f..a41d49b9e 100644 --- a/server/mux.go +++ b/server/mux.go @@ -155,6 +155,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler { router.GET("/chronograf/v1/sources/:id", EnsureViewer(service.SourcesID)) router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource)) router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource)) + router.GET("/chronograf/v1/sources/:id/health", EnsureViewer(service.SourceHealth)) // IFQL router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL)) diff --git a/server/sources.go b/server/sources.go index b6af66ce0..718a62041 100644 --- a/server/sources.go +++ b/server/sources.go @@ -25,6 +25,7 @@ type sourceLinks struct { Roles string `json:"roles,omitempty"` // URL for all users associated with this source Databases string `json:"databases"` // URL for the databases contained within this source Annotations string `json:"annotations"` // URL for the annotations of this source + Health string `json:"health"` // URL for source health } type sourceResponse struct { @@ -55,6 +56,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse { Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID), Databases: fmt.Sprintf("%s/%d/dbs", httpAPISrcs, src.ID), Annotations: fmt.Sprintf("%s/%d/annotations", httpAPISrcs, src.ID), + Health: fmt.Sprintf("%s/%d/health", httpAPISrcs, src.ID), }, } @@ -192,6 +194,38 @@ func (s *Service) RemoveSource(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// SourceHealth determines if the tsdb is running +func (s *Service) SourceHealth(w http.ResponseWriter, r *http.Request) { + id, err := paramID("id", r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) + return + } + + ctx := r.Context() + src, err := s.Store.Sources(ctx).Get(ctx, id) + if err != nil { + notFound(w, id, s.Logger) + return + } + + cli := &influx.Client{ + Logger: s.Logger, + } + + if err := cli.Connect(ctx, &src); err != nil { + Error(w, http.StatusBadRequest, "Error contacting source", s.Logger) + return + } + + if err := cli.Ping(ctx); err != nil { + Error(w, http.StatusBadRequest, "Error contacting source", s.Logger) + return + } + + w.WriteHeader(http.StatusOK) +} + // removeSrcsKapa will remove all kapacitors and kapacitor rules from the stores. // However, it will not remove the kapacitor tickscript from kapacitor itself. func (s *Service) removeSrcsKapa(ctx context.Context, srcID int) error { diff --git a/server/swagger.json b/server/swagger.json index 508ee2be0..57e5368fb 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -387,6 +387,39 @@ } } }, + "/sources/{id}/health": { + "get": { + "tags": ["sources"], + "summary": "Health check for source", + "description": "Returns if the tsdb source can be contacted", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "string", + "description": "ID of the data source", + "required": true + } + ], + "responses": { + "200": { + "description": "Source was able to be contacted" + }, + "404": { + "description": "Source could not be contacted", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "A processing or an unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/sources/{id}/permissions": { "get": { "tags": ["sources", "users"], @@ -3685,7 +3718,8 @@ "queries": "/chronograf/v1/sources/4/queries", "permissions": "/chronograf/v1/sources/4/permissions", "users": "/chronograf/v1/sources/4/users", - "roles": "/chronograf/v1/sources/4/roles" + "roles": "/chronograf/v1/sources/4/roles", + "health": "/chronograf/v1/sources/4/health" } }, "required": ["url"], @@ -3792,6 +3826,11 @@ "description": "Optional path to the roles endpoint IFF it is supported on this source", "format": "url" + }, + "health": { + "type": "string", + "description": "Path to determine if source is healthy", + "format": "url" } } } diff --git a/ui/src/CheckSources.tsx b/ui/src/CheckSources.tsx index 0d01573c2..ff8fdeebc 100644 --- a/ui/src/CheckSources.tsx +++ b/ui/src/CheckSources.tsx @@ -11,7 +11,7 @@ import { ADMIN_ROLE, } from 'src/auth/Authorized' -import {showDatabases} from 'src/shared/apis/metaQuery' +import {getSourceHealth} from 'src/sources/apis' import {getSourcesAsync} from 'src/shared/actions/sources' import {errorThrown as errorThrownAction} from 'src/shared/actions/errors' @@ -87,16 +87,15 @@ class CheckSources extends Component { } public shouldComponentUpdate(nextProps) { - const {auth: {isUsingAuth, me}} = nextProps - // don't update this component if currentOrganization is what has changed, - // or else the app will try to call showDatabases in componentWillUpdate, - // which will fail unless sources have been refreshed + const {location} = nextProps + if ( - isUsingAuth && - me.currentOrganization.id !== this.props.auth.me.currentOrganization.id + !this.state.isFetching && + this.props.location.pathname === location.pathname ) { return false } + return true } @@ -109,7 +108,6 @@ class CheckSources extends Component { sources, auth: {isUsingAuth, me, me: {organizations = [], currentOrganization}}, notify, - getSources, } = nextProps const {isFetching} = nextState const source = sources.find(s => s.id === params.sourceID) @@ -169,22 +167,10 @@ class CheckSources extends Component { } if (!isFetching && !location.pathname.includes('/manage-sources')) { - // Do simple query to proxy to see if the source is up. try { - // the guard around currentOrganization prevents this showDatabases - // invocation since sources haven't been refreshed yet - await showDatabases(source.links.proxy) + await getSourceHealth(source.links.health) } catch (error) { - try { - const newSources = await getSources() - if (newSources.length) { - errorThrown(error, copy.notifySourceNoLongerAvailable(source.name)) - } else { - errorThrown(error, copy.notifyNoSourcesAvailable(source.name)) - } - } catch (error2) { - errorThrown(error2, copy.notifyUnableToRetrieveSources()) - } + errorThrown(error, copy.notifySourceNoLongerAvailable(source.name)) } } } diff --git a/ui/src/shared/copy/notifications.js b/ui/src/shared/copy/notifications.js index 385895312..4a0f5dc19 100644 --- a/ui/src/shared/copy/notifications.js +++ b/ui/src/shared/copy/notifications.js @@ -133,7 +133,7 @@ export const notifySourceDeleteFailed = sourceName => ({ }) export const notifySourceNoLongerAvailable = sourceName => - `Source ${sourceName} is no longer available. Successfully connected to another source.` + `Source ${sourceName} is no longer available. Please ensure InfluxDB is running.` export const notifyNoSourcesAvailable = sourceName => `Unable to connect to source ${sourceName}. No other sources available.` diff --git a/ui/src/sources/apis/index.ts b/ui/src/sources/apis/index.ts new file mode 100644 index 000000000..9c528aa71 --- /dev/null +++ b/ui/src/sources/apis/index.ts @@ -0,0 +1,10 @@ +import AJAX from 'src/utils/ajax' + +export const getSourceHealth = async (url: string) => { + try { + await AJAX({url}) + } catch (error) { + console.error(`Unable to contact source ${url}`, error) + throw error + } +}