-
Notifications
You must be signed in to change notification settings - Fork 191
Eureka health status #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 8 commits
4cf2db5
156c236
2330089
6844d6a
ff56d21
4b0b899
6c33822
805c89d
1dc1319
d61a354
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ | |
* [SonarQube](#sonarqube) | ||
* [Elasticsearch Hit Count](#elasticsearch-hit-count) | ||
* [GitHub Issue Count](#github-issue-count) | ||
* [Eureka Health Status](#eureka-health-status) | ||
* [Available Themes](#available-themes) | ||
* [light](#light) | ||
* [dark](#dark) | ||
|
@@ -312,6 +313,35 @@ import GitHubIssueCount from '../components/github/issue-count' | |
* `repository`: Name of the repository | ||
* `authKey`: Credential key, defined in [auth.js](./auth.js) | ||
|
||
### [Eureka Health Status](./components/widgets/eureka/health-status.js) | ||
|
||
#### Example | ||
|
||
```javascript | ||
import EurekaHealthStatus from '../components/widgets/eureka/health-status' | ||
|
||
<EurekaHealthStatus | ||
title='Eureka' | ||
url='http://127.0.0.1:8080/' | ||
baseQuery='http://eurekahost:8761' | ||
healthQuery='/management/health' | ||
appsQuery='/eureka/apps' | ||
appNamePattern='SERVICE' | ||
minimumInstances={2} | ||
/> | ||
``` | ||
|
||
#### props | ||
|
||
* `title`: Widget title (Default: `GitHub Issue Count`) | ||
* `interval`: Refresh interval in milliseconds (Default: `300000`) | ||
|
||
* `url`: Eureka Server Base URL | ||
* `healthQuery`: Relative Path to Spring Boot Actuator Health endpoint | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Documentation for |
||
* `appsQuery`: Relative Path to Eureka Apps API endpoint [Eureka REST operations](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations) | ||
|
||
* `authKey`: Credential key, defined in [auth.js](./auth.js) | ||
* `appNamePattern`: Name pattern the service-names have to start with | ||
* `minimumInstances`: Number of instances for each service which are expected to run to be fine | ||
|
||
|
||
## Available Themes | ||
|
||
### [light](./styles/light-theme.js) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { Component } from 'react' | ||
import fetch from 'isomorphic-unfetch' | ||
import yup from 'yup' | ||
import Widget from '../../widget' | ||
import Table, { Th, Td } from '../../table' | ||
import { basicAuthHeader } from '../../../lib/auth' | ||
import styled from 'styled-components' | ||
|
||
const schema = yup.object().shape({ | ||
url: yup.string().url().required(), | ||
interval: yup.number(), | ||
baseQuery: yup.string().required(), | ||
appsQuery: yup.string().required(), | ||
healthQuery: yup.string().required(), | ||
title: yup.string(), | ||
minimumInstances: yup.number(), | ||
appNamePattern: yup.string(), | ||
authKey: yup.string() | ||
}) | ||
|
||
const EurekaDiv = styled.div` | ||
background-color: ${props => props.hasError ? props.theme.palette.errorColor : props.theme.palette.canvasColor}; | ||
` | ||
|
||
export default class EurekaHealthStatus extends Component { | ||
static defaultProps = { | ||
interval: 1000 * 60 * 60, | ||
title: 'Eureka Health Status', | ||
minimumInstances: 2, | ||
appNamePattern: '' | ||
} | ||
|
||
state = { | ||
error: false, | ||
loading: true, | ||
appStatus: '', | ||
infoMessage: '' | ||
} | ||
|
||
componentDidMount () { | ||
schema.validate(this.props) | ||
.then(() => this.fetchInformation()) | ||
.catch((err) => { | ||
console.error(`${err.name} @ ${this.constructor.name}`, err.errors) | ||
this.setState({ error: true, loading: false }) | ||
}) | ||
} | ||
|
||
componentWillUnmount () { | ||
clearInterval(this.interval) | ||
} | ||
|
||
checkInstanceCount (minimumInstances, appNamePattern, appList) { | ||
let hasError = false | ||
appList.forEach(function (entry) { | ||
if (entry.name.startsWith(appNamePattern) && entry.instance.length < minimumInstances) { | ||
hasError = true | ||
} | ||
}) | ||
return hasError | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have an look to Untested: checkInstanceCount (element) {
const { appNamePattern, minimumInstances } = this.props
return element.name.startsWith(appNamePattern) && element.instance.length < minimumInstances
}
appList.every(this.checkInstanceCount) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. every is not working because it stops with the first |
||
|
||
async checkInstanceHealth (url, appNamePattern, appList) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can optimize this function in the next step. @chrishelgert Can you support @michl-b, please? |
||
let hasError = false | ||
for (var i = 0; i < appList.length; i++) { | ||
const app = appList[i] | ||
if (app.name.startsWith(appNamePattern)) { | ||
for (var j = 0; j < app.instance.length; j++) { | ||
try { | ||
const curInstance = app.instance[j] | ||
let opts = {headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }} | ||
const resHealth = await fetch(url + curInstance.healthCheckUrl, opts) | ||
const healthJson = await resHealth.json() | ||
hasError = healthJson.status !== 'UP' | ||
} catch (error) { | ||
hasError = true | ||
} | ||
} | ||
} | ||
} | ||
return hasError | ||
} | ||
|
||
async fetchInformation () { | ||
const { authKey, url, baseQuery, healthQuery, appsQuery, appNamePattern, minimumInstances } = this.props | ||
let opts = authKey ? { headers: basicAuthHeader(authKey) } : {} | ||
|
||
try { | ||
const res = await fetch(`${url}${baseQuery}${healthQuery}`, opts) | ||
const json = await res.json() | ||
|
||
let appStatus = '' | ||
let infoMessage = '' | ||
let hasError = json.status !== 'UP' | ||
|
||
if (hasError === false) { | ||
|
||
opts = {headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }} | ||
|
||
try { | ||
const resApps = await fetch(`${url}${baseQuery}${appsQuery}`, opts) | ||
const jsonApps = await resApps.json() | ||
|
||
hasError = jsonApps.applications.apps__hashcode.includes('DOWN') | ||
|
||
const appsStatus = jsonApps.applications.apps__hashcode.split('_') | ||
if (appsStatus.length > 0 && appsStatus.length < 4) { | ||
appStatus = `${appsStatus[0]}: ${appsStatus[1]}` | ||
} else { | ||
appStatus = `${appsStatus[0]}: ${appsStatus[1]}` | ||
appStatus += ` - ${appsStatus[2]}: ${appsStatus[3]}` | ||
|
||
} | ||
|
||
if (hasError === false) { | ||
|
||
hasError = await this.checkInstanceCount(minimumInstances, appNamePattern, jsonApps.applications.application) | ||
|
||
if (hasError === true) { | ||
|
||
infoMessage = 'Instance redundancy failed' | ||
} | ||
} | ||
|
||
if (hasError === false) { | ||
|
||
hasError = await this.checkInstanceHealth(url, appNamePattern, jsonApps.applications.application) | ||
if (hasError === true) { | ||
|
||
infoMessage = 'App health check failed' | ||
} | ||
} | ||
} catch (error) { | ||
hasError = true | ||
} | ||
} else { | ||
infoMessage = 'Eureka health failed' | ||
} | ||
this.setState({ appStatus, infoMessage, hasError, error: false, loading: false }) | ||
} catch (error) { | ||
this.setState({ error: true, loading: false }) | ||
} finally { | ||
this.interval = setInterval(() => this.fetchInformation(), this.props.interval) | ||
|
||
} | ||
} | ||
|
||
render () { | ||
const { error, loading, appStatus, infoMessage, hasError } = this.state | ||
const { title } = this.props | ||
|
||
return ( | ||
<Widget title={title} loading={loading} error={error}> | ||
<EurekaDiv hasError={hasError}> | ||
<Table> | ||
<tbody> | ||
<tr> | ||
<Th>Apps</Th> | ||
<Td>{appStatus}</Td> | ||
</tr> | ||
<tr> | ||
<Th>Info</Th> | ||
<Td>{infoMessage}</Td> | ||
</tr> | ||
</tbody> | ||
</Table> | ||
</EurekaDiv> | ||
</Widget> | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Default title ->
Eureka Health Status