Introduce /health endpoint

pull/3118/head
Andrew Watkins 2018-04-03 15:58:33 -07:00
parent d99f7720ee
commit 5f5a8c2ee5
6 changed files with 94 additions and 24 deletions

View File

@ -155,6 +155,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/sources/:id", EnsureViewer(service.SourcesID)) router.GET("/chronograf/v1/sources/:id", EnsureViewer(service.SourcesID))
router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource)) router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource))
router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource)) router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource))
router.GET("/chronograf/v1/sources/:id/health", EnsureViewer(service.SourceHealth))
// IFQL // IFQL
router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL)) router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL))

View File

@ -25,6 +25,7 @@ type sourceLinks struct {
Roles string `json:"roles,omitempty"` // URL for all users associated with this source 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 Databases string `json:"databases"` // URL for the databases contained within this source
Annotations string `json:"annotations"` // URL for the annotations of this source Annotations string `json:"annotations"` // URL for the annotations of this source
Health string `json:"health"` // URL for source health
} }
type sourceResponse struct { type sourceResponse struct {
@ -55,6 +56,7 @@ func newSourceResponse(src chronograf.Source) sourceResponse {
Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID), Users: fmt.Sprintf("%s/%d/users", httpAPISrcs, src.ID),
Databases: fmt.Sprintf("%s/%d/dbs", httpAPISrcs, src.ID), Databases: fmt.Sprintf("%s/%d/dbs", httpAPISrcs, src.ID),
Annotations: fmt.Sprintf("%s/%d/annotations", 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) 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. // removeSrcsKapa will remove all kapacitors and kapacitor rules from the stores.
// However, it will not remove the kapacitor tickscript from kapacitor itself. // However, it will not remove the kapacitor tickscript from kapacitor itself.
func (s *Service) removeSrcsKapa(ctx context.Context, srcID int) error { func (s *Service) removeSrcsKapa(ctx context.Context, srcID int) error {

View File

@ -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": { "/sources/{id}/permissions": {
"get": { "get": {
"tags": ["sources", "users"], "tags": ["sources", "users"],
@ -3685,7 +3718,8 @@
"queries": "/chronograf/v1/sources/4/queries", "queries": "/chronograf/v1/sources/4/queries",
"permissions": "/chronograf/v1/sources/4/permissions", "permissions": "/chronograf/v1/sources/4/permissions",
"users": "/chronograf/v1/sources/4/users", "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"], "required": ["url"],
@ -3792,6 +3826,11 @@
"description": "description":
"Optional path to the roles endpoint IFF it is supported on this source", "Optional path to the roles endpoint IFF it is supported on this source",
"format": "url" "format": "url"
},
"health": {
"type": "string",
"description": "Path to determine if source is healthy",
"format": "url"
} }
} }
} }

View File

@ -11,7 +11,7 @@ import {
ADMIN_ROLE, ADMIN_ROLE,
} from 'src/auth/Authorized' } 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 {getSourcesAsync} from 'src/shared/actions/sources'
import {errorThrown as errorThrownAction} from 'src/shared/actions/errors' import {errorThrown as errorThrownAction} from 'src/shared/actions/errors'
@ -87,16 +87,15 @@ class CheckSources extends Component<Props, State> {
} }
public shouldComponentUpdate(nextProps) { public shouldComponentUpdate(nextProps) {
const {auth: {isUsingAuth, me}} = nextProps const {location} = 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
if ( if (
isUsingAuth && !this.state.isFetching &&
me.currentOrganization.id !== this.props.auth.me.currentOrganization.id this.props.location.pathname === location.pathname
) { ) {
return false return false
} }
return true return true
} }
@ -109,7 +108,6 @@ class CheckSources extends Component<Props, State> {
sources, sources,
auth: {isUsingAuth, me, me: {organizations = [], currentOrganization}}, auth: {isUsingAuth, me, me: {organizations = [], currentOrganization}},
notify, notify,
getSources,
} = nextProps } = nextProps
const {isFetching} = nextState const {isFetching} = nextState
const source = sources.find(s => s.id === params.sourceID) const source = sources.find(s => s.id === params.sourceID)
@ -169,22 +167,10 @@ class CheckSources extends Component<Props, State> {
} }
if (!isFetching && !location.pathname.includes('/manage-sources')) { if (!isFetching && !location.pathname.includes('/manage-sources')) {
// Do simple query to proxy to see if the source is up.
try { try {
// the guard around currentOrganization prevents this showDatabases await getSourceHealth(source.links.health)
// invocation since sources haven't been refreshed yet
await showDatabases(source.links.proxy)
} catch (error) { } catch (error) {
try {
const newSources = await getSources()
if (newSources.length) {
errorThrown(error, copy.notifySourceNoLongerAvailable(source.name)) errorThrown(error, copy.notifySourceNoLongerAvailable(source.name))
} else {
errorThrown(error, copy.notifyNoSourcesAvailable(source.name))
}
} catch (error2) {
errorThrown(error2, copy.notifyUnableToRetrieveSources())
}
} }
} }
} }

View File

@ -133,7 +133,7 @@ export const notifySourceDeleteFailed = sourceName => ({
}) })
export const notifySourceNoLongerAvailable = 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 => export const notifyNoSourcesAvailable = sourceName =>
`Unable to connect to source ${sourceName}. No other sources available.` `Unable to connect to source ${sourceName}. No other sources available.`

View File

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