Introduce /health endpoint
parent
d99f7720ee
commit
5f5a8c2ee5
|
@ -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))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.`
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue