diff --git a/README.md b/README.md
index 31680e7b..f3c27ab1 100644
--- a/README.md
+++ b/README.md
@@ -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,36 @@ 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'
+
+
+```
+
+#### props
+
+* `title`: Widget title (Default: `Eureka Health Status`)
+* `interval`: Refresh interval in milliseconds (Default: `360000`)
+* `url`: Cross Origin Service URL
+* `eurekaQuery`: Eureka Server URL
+* `healthQuery`: Relative Path to Spring Boot Actuator Health endpoint
+* `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 (Default: `2`)
+
## Available Themes
### [light](./styles/light-theme.js)
diff --git a/components/widgets/eureka/health-status.js b/components/widgets/eureka/health-status.js
new file mode 100644
index 00000000..6aa39a6b
--- /dev/null
+++ b/components/widgets/eureka/health-status.js
@@ -0,0 +1,168 @@
+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(),
+ eurekaQuery: 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: ''
+ }
+
+ constructor (props, context) {
+ super(props, context)
+ this.checkInstanceCount = this.checkInstanceCount.bind(this)
+ }
+
+ 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 () {
+ clearTimeout(this.timeout)
+ }
+
+ checkInstanceCount (appList) {
+ const { appNamePattern, minimumInstances } = this.props
+ let hasError = false
+ appList.forEach(function (entry) {
+ if ((appNamePattern.length === 0 || entry.name.startsWith(appNamePattern)) && entry.instance.length < minimumInstances) {
+ hasError = true
+ }
+ })
+ return hasError
+ }
+
+ async checkInstanceHealth (url, appNamePattern, appList) {
+ let hasError = false
+ for (var i = 0; i < appList.length; i++) {
+ const app = appList[i]
+ if (appNamePattern.length === 0 || 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, eurekaQuery, healthQuery, appsQuery, appNamePattern } = this.props
+ let opts = authKey ? { headers: basicAuthHeader(authKey) } : {}
+
+ try {
+ const res = await fetch(`${url}${eurekaQuery}${healthQuery}`, opts)
+ const json = await res.json()
+
+ let appStatus = ''
+ let infoMessage = ''
+ let hasError = json.status !== 'UP'
+
+ if (!hasError) {
+ opts = {headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }}
+
+ try {
+ const resApps = await fetch(`${url}${eurekaQuery}${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]} - ${appsStatus[2]}: ${appsStatus[3]}`
+ }
+
+ if (!hasError) {
+ hasError = this.checkInstanceCount(jsonApps.applications.application)
+ if (hasError) {
+ infoMessage = 'Instance redundancy failed'
+ }
+ }
+
+ if (!hasError) {
+ hasError = await this.checkInstanceHealth(url, appNamePattern, jsonApps.applications.application)
+ if (hasError) {
+ 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.timeout = setTimeout(() => this.fetchInformation(), this.props.interval)
+ }
+ }
+
+ render () {
+ const { error, loading, appStatus, infoMessage, hasError } = this.state
+ const { title } = this.props
+
+ return (
+
+
+
+
+
+ Apps |
+ {appStatus} |
+
+
+ Info |
+ {infoMessage} |
+
+
+
+
+
+ )
+ }
+}