-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This service is in charge of monitoring the remote status for applications
- Loading branch information
1 parent
a767e9c
commit 08d16e2
Showing
8 changed files
with
340 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
FROM golang:alpine AS builder | ||
RUN mkdir -p /go/src/app | ||
WORKDIR /go/src/app | ||
ADD --chown=65535:65535 . . | ||
RUN go build -o bin/status | ||
|
||
FROM alpine:latest | ||
COPY --chown=65535:65535 --from=builder /go/src/app/bin/status /status | ||
USER 65535 | ||
ENTRYPOINT /status |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# status service | ||
|
||
This service takes a comma separated list of key=value pairs and returns the status of the services in the list via the `/status` endpoint. | ||
|
||
## Configuration | ||
|
||
| Environment variable | Type | Default | Description | | ||
| -------------------- | ---- | ------- | ----------- | | ||
| `LISTEN_ADDRESS` | string | `:8082` | Address to listen on for HTTP requests. | | ||
| `EXTERNAL_SERVICES_TO_WATCH` | []string | `redhat=https://www.redhat.com/en` | Comma separated list of key=value pairs of services to watch | | ||
| `CHECK_INTERVAL` | duration | `5s` | Interval parsed as duration, to check the upstream service. | | ||
| `REQUEST_TIMEOUT` | duration | `2s` | Timeout parsed as duration, for the HTTP request to the upstream service. | | ||
|
||
## API | ||
|
||
| Path | Method | Description | | ||
| ---- | ------ | ----------- | | ||
| `/status` | GET | Returns the status of the services in the list. | | ||
| `/healthz` | GET | Returns the health of the service. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/leonsteinhaeuser/rh-ocp-examples/services/status | ||
|
||
go 1.23.2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"log/slog" | ||
"net/http" | ||
"os" | ||
"strings" | ||
"sync" | ||
"time" | ||
) | ||
|
||
var ( | ||
// envExternalServicesToWatch is a comma-separated list of key=value services to watch (HTTP GET). | ||
// The key is the name of the service, and the value is the URL to check. | ||
envExternalServicesToWatch = os.Getenv("EXTERNAL_SERVICES_TO_WATCH") | ||
envCheckInterval = os.Getenv("CHECK_INTERVAL") | ||
envRequestTimeout = os.Getenv("REQUEST_TIMEOUT") | ||
envListenAddress = os.Getenv("LISTEN_ADDRESS") | ||
|
||
externalServicesToWatch = map[string]string{} | ||
checkInterval = 15 * time.Second | ||
requestTimeout = 5 * time.Second | ||
|
||
mlock sync.Mutex = sync.Mutex{} | ||
externalServicesStatus = map[string]ServiceStatus{} | ||
) | ||
|
||
type ServiceStatus struct { | ||
LastChecked time.Time `json:"last_checked"` | ||
Status Status `json:"status"` | ||
URL string `json:"url"` | ||
} | ||
|
||
type Status string | ||
|
||
const ( | ||
Unknown Status = "unknown" | ||
Available Status = "available" | ||
Unavailable Status = "unavailable" | ||
) | ||
|
||
func init() { | ||
// Parse the envExternalServicesToWatch and populate the externalServicesToWatch map. | ||
// If the envExternalServicesToWatch is empty, then we should watch the default services. | ||
if envExternalServicesToWatch == "" { | ||
externalServicesToWatch = map[string]string{ | ||
"redhat": "https://www.redhat.com/en", | ||
} | ||
} else { | ||
services := strings.Split(envExternalServicesToWatch, ",") | ||
for _, service := range services { | ||
parts := strings.Split(service, "=") | ||
if len(parts) != 2 { | ||
// this is a fatal error, so we exit the program. | ||
slog.Error("invalid external service format, expected key=value format", "service", service) | ||
os.Exit(1) | ||
} | ||
externalServicesToWatch[parts[0]] = parts[1] | ||
} | ||
} | ||
|
||
// parse the envCheckInterval and set the checkInterval. | ||
if envCheckInterval != "" { | ||
d, err := time.ParseDuration(envCheckInterval) | ||
if err != nil { | ||
slog.Error("failed to parse the check interval", "interval", envCheckInterval, "error", err) | ||
os.Exit(1) | ||
} | ||
checkInterval = d | ||
} | ||
|
||
// parse the envRequestTimeout and set the requestTimeout. | ||
if envRequestTimeout != "" { | ||
d, err := time.ParseDuration(envRequestTimeout) | ||
if err != nil { | ||
slog.Error("failed to parse the request timeout", "timeout", envRequestTimeout, "error", err) | ||
os.Exit(1) | ||
} | ||
requestTimeout = d | ||
} | ||
http.DefaultClient.Timeout = requestTimeout | ||
} | ||
|
||
func main() { | ||
http.HandleFunc("GET /status", statusHandler) | ||
http.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusOK) | ||
}) | ||
|
||
ctx, cf := context.WithCancel(context.Background()) | ||
defer cf() | ||
// check the availability of the external services. | ||
watcher(ctx) | ||
|
||
slog.Info("starting the server", "address", envListenAddress) | ||
err := http.ListenAndServe(envListenAddress, nil) | ||
if err != nil { | ||
slog.Error("failed to start the server", "error", err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func watcher(ctx context.Context) { | ||
for service, url := range externalServicesToWatch { | ||
go func(ctx context.Context, service, url string) { | ||
ticker := time.NewTicker(checkInterval) | ||
for { | ||
select { | ||
case <-ctx.Done(): | ||
slog.Warn("received a signal to stop the watcher", "service", service) | ||
return | ||
case <-ticker.C: | ||
mlock.Lock() | ||
svc := ServiceStatus{ | ||
LastChecked: time.Now(), | ||
Status: Unknown, | ||
URL: url, | ||
} | ||
err := checkExternalServiceAvailability(service, url) | ||
if err != nil { | ||
svc.Status = Unavailable | ||
externalServicesStatus[service] = svc | ||
mlock.Unlock() | ||
slog.Error("service is not available", "service", service, "error", err) | ||
continue | ||
} | ||
svc.Status = Available | ||
externalServicesStatus[service] = svc | ||
mlock.Unlock() | ||
} | ||
} | ||
}(ctx, service, url) | ||
} | ||
} | ||
|
||
// checkExternalServicesAvailability checks the availability of the external services. | ||
// It sends an HTTP GET request to the URL of the service and checks the status code. | ||
func checkExternalServiceAvailability(service string, url string) error { | ||
rsp, err := http.Get(url) | ||
if err != nil { | ||
return fmt.Errorf("failed to check the availability of the service %s: %w", service, err) | ||
} | ||
if rsp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("service %s is not available: %s", service, rsp.Status) | ||
} | ||
return nil | ||
} | ||
|
||
func statusHandler(w http.ResponseWriter, r *http.Request) { | ||
err := json.NewEncoder(w).Encode(externalServicesStatus) | ||
if err != nil { | ||
slog.Error("failed to encode the response", "error", err) | ||
http.Error(w, "internal server error", http.StatusInternalServerError) | ||
return | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.