Skip to content

Commit

Permalink
Health Checks V2 API (#8)
Browse files Browse the repository at this point in the history
* Health Checks V2 API

* Split V1 and V2 APIs in http.go

* Switch case

* V2 does not have traverse

* V2 does not have traverse

* More updates to V2

* Updates to tests

* Updates to support V2 About

* Small fixes

* Fix serialize status list

* Correct serialize code, fix AmIUpV2 test

* More http tests for V2

* Make aggregate details match new

* address feedback

* invalid api version error from about

* added error for about and introduced APIVersion type

* small copy fix

* typo fix

* add aggregate v2 tests

* add aboutv2 test

* fix about deps

* add test for about check status false
  • Loading branch information
kush-patel-hs authored and HootAdam committed Feb 4, 2019
1 parent f789877 commit d87594e
Show file tree
Hide file tree
Showing 10 changed files with 636 additions and 80 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ _test
build
src
.idea
.vscode

# Architecture specific extensions/prefixes
*.[568vq]
Expand All @@ -27,4 +28,4 @@ _testmain.go
*.prof

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.out
214 changes: 209 additions & 5 deletions about.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package healthchecks

import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -44,6 +45,22 @@ type AboutResponse struct {
CustomData map[string]interface{} `json:"customData"`
}

type AboutResponseV2 struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Protocol string `json:"protocol"`
Owners []string `json:"owners"`
Version string `json:"version"`
Host string `json:"host"`
ProjectRepo string `json:"projectRepo"`
ProjectHome string `json:"projectHome"`
LogsLinks []string `json:"logsLinks"`
StatsLinks []string `json:"statsLinks"`
Dependencies []DependencyInfo `json:"dependencies"`
CustomData map[string]interface{} `json:"customData"`
}

type Dependency struct {
Name string `json:"name"`
Status []JsonResponse `json:"status"`
Expand All @@ -53,11 +70,25 @@ type Dependency struct {
IsTraversable bool `json:"isTraversable"`
}

type DependencyInfo struct {
Name string `json:"name"`
Status JsonResponse `json:"status"`
StatusDuration float64 `json:"statusDuration"`
StatusPath string `json:"statusPath"`
Type string `json:"type"`
IsTraversable bool `json:"isTraversable"`
}

type dependencyPosition struct {
item Dependency
position int
}

type dependencyInfoPosition struct {
item DependencyInfo
position int
}

func getAboutFieldValue(aboutConfigMap map[string]interface{}, key string, aboutFilePath string) string {
value, ok := aboutConfigMap[key]
if !ok {
Expand Down Expand Up @@ -118,7 +149,30 @@ func getAboutCustomDataFieldValues(aboutConfigMap map[string]interface{}, aboutF
return mapValue
}

func About(statusEndpoints []StatusEndpoint, protocol string, aboutFilePath string, versionFilePath string, customData map[string]interface{}) string {
func About(
statusEndpoints []StatusEndpoint,
protocol string, aboutFilePath string,
versionFilePath string,
customData map[string]interface{},
apiVersion APIVersion,
checkStatus bool,
) (string, error) {
switch apiVersion {
case APIV1:
return aboutV1(statusEndpoints, protocol, aboutFilePath, versionFilePath, customData), nil
case APIV2:
return aboutV2(statusEndpoints, protocol, aboutFilePath, versionFilePath, customData, checkStatus), nil
default:
return "", errors.New("Invalid API Version")
}
}

func aboutV1(
statusEndpoints []StatusEndpoint,
protocol string, aboutFilePath string,
versionFilePath string,
customData map[string]interface{},
) string {
aboutData, _ := ioutil.ReadFile(aboutFilePath)

// Initialize ConfigAbout with default values in case we have problems reading from the file
Expand Down Expand Up @@ -205,7 +259,7 @@ func About(statusEndpoints []StatusEndpoint, protocol string, aboutFilePath stri
go func(s StatusEndpoint, i int) {
start := time.Now()
dependencyStatus := translateStatusList(s.StatusCheck.CheckStatus(s.Name))
var elapsed float64 = float64(time.Since(start)) * 0.000000001
elapsed := float64(time.Since(start)) * 0.000000001
dependency := Dependency{
Name: s.Name,
Status: dependencyStatus,
Expand Down Expand Up @@ -237,16 +291,166 @@ func About(statusEndpoints []StatusEndpoint, protocol string, aboutFilePath stri

aboutResponse.Dependencies = dependencies

aboutResponseJson, err := json.Marshal(aboutResponse)
aboutResponseJSON, err := json.Marshal(aboutResponse)
if err != nil {
msg := fmt.Sprintf("Error serializing AboutResponse: %s", err)
sl := StatusList{
StatusList: []Status{
{Description: "Invalid AboutResponse", Result: CRITICAL, Details: msg},
},
}
return SerializeStatusList(sl, APIV1)
}

return string(aboutResponseJSON)
}

func aboutV2(
statusEndpoints []StatusEndpoint,
protocol string, aboutFilePath string,
versionFilePath string,
customData map[string]interface{},
checkStatus bool,
) string {
aboutData, _ := ioutil.ReadFile(aboutFilePath)

// Initialize ConfigAbout with default values in case we have problems reading from the file
aboutConfig := ConfigAbout{
Id: ABOUT_FIELD_NA,
Summary: ABOUT_FIELD_NA,
Description: ABOUT_FIELD_NA,
Maintainers: []string{},
ProjectRepo: ABOUT_FIELD_NA,
ProjectHome: ABOUT_FIELD_NA,
LogsLinks: []string{},
StatsLinks: []string{},
}

// Unmarshal JSON into a generic object so we don't completely fail if one of the fields is invalid or missing
var aboutConfigMap map[string]interface{}
err := json.Unmarshal(aboutData, &aboutConfigMap)

if err == nil {
// Parse out each value individually
aboutConfig.Id = getAboutFieldValue(aboutConfigMap, "id", aboutFilePath)
aboutConfig.Summary = getAboutFieldValue(aboutConfigMap, "summary", aboutFilePath)
aboutConfig.Description = getAboutFieldValue(aboutConfigMap, "description", aboutFilePath)
aboutConfig.Maintainers = getAboutFieldValues(aboutConfigMap, "maintainers", aboutFilePath)
aboutConfig.ProjectRepo = getAboutFieldValue(aboutConfigMap, "projectRepo", aboutFilePath)
aboutConfig.ProjectHome = getAboutFieldValue(aboutConfigMap, "projectHome", aboutFilePath)
aboutConfig.LogsLinks = getAboutFieldValues(aboutConfigMap, "logsLinks", aboutFilePath)
aboutConfig.StatsLinks = getAboutFieldValues(aboutConfigMap, "statsLinks", aboutFilePath)
aboutConfig.CustomData = getAboutCustomDataFieldValues(aboutConfigMap, aboutFilePath)
} else {
fmt.Printf("Error deserializing about data from %s. Error: %s JSON: %s\n", aboutFilePath, err.Error(), aboutData)
}

// Merge custom data from about.json with custom data passed in by client
// and prefer values passed by client over values in about.json
if customData != nil {
if aboutConfig.CustomData == nil {
aboutConfig.CustomData = make(map[string]interface{})
}

for key, value := range customData {
aboutConfig.CustomData[key] = value
}
}

// Extract version
var version string
versionData, err := ioutil.ReadFile(versionFilePath)
if err != nil {
fmt.Printf("Error reading version from %s. Error: %s\n", versionFilePath, err.Error())
version = VERSION_NA
} else {
version = strings.TrimSpace(string(versionData))
}

// Get hostname
host, err := os.Hostname()
if err != nil {
fmt.Printf("Error getting hostname. Error: %s\n", err.Error())
host = "unknown"
}

aboutResponse := AboutResponseV2{
Id: aboutConfig.Id,
Name: aboutConfig.Summary,
Description: aboutConfig.Description,
Protocol: protocol,
Owners: aboutConfig.Maintainers,
Version: version,
Host: host,
ProjectRepo: aboutConfig.ProjectRepo,
ProjectHome: aboutConfig.ProjectHome,
LogsLinks: aboutConfig.LogsLinks,
StatsLinks: aboutConfig.StatsLinks,
CustomData: aboutConfig.CustomData,
}

dependencies := make([]DependencyInfo, len(statusEndpoints))
if checkStatus {
// Execute status checks async
var wg sync.WaitGroup
dc := make(chan dependencyInfoPosition)
wg.Add(len(statusEndpoints))

for ie, se := range statusEndpoints {
go func(s StatusEndpoint, i int) {
start := time.Now()
dependencyStatus := translateStatusListV2(s.StatusCheck.CheckStatus(s.Name))
elapsed := float64(time.Since(start)) * 0.000000001
dependency := DependencyInfo{
Name: s.Name,
Status: dependencyStatus,
StatusDuration: elapsed,
StatusPath: s.Slug,
Type: s.Type,
IsTraversable: s.IsTraversable,
}

dc <- dependencyInfoPosition{
item: dependency,
position: i,
}
}(se, ie)
}

// Collect our responses and put them in the right spot
go func() {
for dp := range dc {
dependencies[dp.position] = dp.item
wg.Done()
}
}()

// Wait until all async status checks are done and collected
wg.Wait()
close(dc)
} else {
for index, statusEndpoint := range statusEndpoints {
dependencies[index] = DependencyInfo{
Name: statusEndpoint.Name,
StatusPath: statusEndpoint.Slug,
Type: statusEndpoint.Type,
IsTraversable: statusEndpoint.IsTraversable,
}
}
}

aboutResponse.Dependencies = dependencies

aboutResponseJSON, err := json.Marshal(aboutResponse)
if err != nil {
msg := fmt.Sprintf("Error serializing AboutResponse: %s", err)
sl := StatusList{
StatusList: []Status{
{Description: "Invalid AboutResponse", Result: CRITICAL, Details: msg},
},
}
return SerializeStatusList(sl)
return SerializeStatusList(sl, APIV2)
}

return string(aboutResponseJson)
return string(aboutResponseJSON)
}
Loading

0 comments on commit d87594e

Please sign in to comment.