Skip to content

Commit

Permalink
Merge pull request #25 from appuio/add-healthz-endpoint
Browse files Browse the repository at this point in the history
Add `/healthz` endpoint that checks Alertmanager connection
  • Loading branch information
bastjan authored Jun 24, 2024
2 parents eb25cbf + e96bc8a commit bd9a972
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 2 deletions.
7 changes: 7 additions & 0 deletions config/exporter/exporter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,12 @@ spec:
requests:
cpu: 10m
memory: 64Mi
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 20
initialDelaySeconds: 15
timeoutSeconds: 3
serviceAccountName: alerts-exporter
terminationGracePeriodSeconds: 10
45 changes: 45 additions & 0 deletions internal/healthcheck/healthcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package healthcheck

import (
"encoding/json"
"net/http"

"github.com/go-openapi/strfmt"
"github.com/prometheus/alertmanager/api/v2/client/general"
"github.com/prometheus/alertmanager/api/v2/models"
)

// HealthCheck is a health check handler for the Alertmanager API.
type HealthCheck struct {
GeneralService general.ClientService
}

// HandleHealthz handles a health check request.
// It returns a JSON response with the status of the Alertmanager API or an error if the client returns an error or if receiving a nil response.
func (h HealthCheck) HandleHealthz(res http.ResponseWriter, req *http.Request) {
ams, err := h.GeneralService.GetStatus(general.NewGetStatusParamsWithContext(req.Context()))
if err != nil {
http.Error(res, err.Error(), http.StatusInternalServerError)
return
}
if ams == nil || ams.Payload == nil {
http.Error(res, "Nil response from Alertmanager", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(res).Encode(response{
Status: "connected",
AlertmanagerCluster: ams.Payload.Cluster,
AlertmanagerVersion: ams.Payload.VersionInfo,
AlertmanagerUptime: ams.Payload.Uptime,
}); err != nil {
http.Error(res, "Encoding error: "+err.Error(), http.StatusInternalServerError)
return
}
}

type response struct {
Status string `json:"status"`
AlertmanagerCluster *models.ClusterStatus `json:"alertmanager_cluster"`
AlertmanagerVersion *models.VersionInfo `json:"alertmanager_version"`
AlertmanagerUptime *strfmt.DateTime `json:"alertmanager_uptime"`
}
85 changes: 85 additions & 0 deletions internal/healthcheck/healthcheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package healthcheck_test

import (
"errors"
"net/http"
"net/http/httptest"
"testing"

"github.com/appuio/alerts_exporter/internal/healthcheck"
"github.com/go-openapi/runtime"
"github.com/prometheus/alertmanager/api/v2/client/general"
"github.com/prometheus/alertmanager/api/v2/models"
"github.com/stretchr/testify/require"
)

func TestOk(t *testing.T) {
t.Parallel()

hc := &healthcheck.HealthCheck{
GeneralService: &mockClientService{
OkResponse: &general.GetStatusOK{
Payload: &models.AlertmanagerStatus{
VersionInfo: &models.VersionInfo{
Version: ptr("v0.22.2"),
},
},
},
},
}

req := httptest.NewRecorder()
hc.HandleHealthz(req, httptest.NewRequest("GET", "/healthz", nil))
res := req.Result()
require.Equal(t, http.StatusOK, res.StatusCode)
require.Contains(t, req.Body.String(), `"status":"connected"`)
require.Contains(t, req.Body.String(), `v0.22.2`)
}

func TestErrResponse(t *testing.T) {
t.Parallel()

hc := &healthcheck.HealthCheck{
GeneralService: &mockClientService{
Err: errors.New("some error"),
},
}

req := httptest.NewRecorder()
hc.HandleHealthz(req, httptest.NewRequest("GET", "/healthz", nil))
res := req.Result()
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
require.Contains(t, req.Body.String(), "some error")
}

func TestNilResponse(t *testing.T) {
t.Parallel()

hc := &healthcheck.HealthCheck{
GeneralService: &mockClientService{},
}

req := httptest.NewRecorder()
hc.HandleHealthz(req, httptest.NewRequest("GET", "/healthz", nil))
res := req.Result()
require.Equal(t, http.StatusInternalServerError, res.StatusCode)
require.Contains(t, req.Body.String(), "Nil response")
}

type mockClientService struct {
OkResponse *general.GetStatusOK
Err error
}

var _ general.ClientService = (*mockClientService)(nil)

func (m *mockClientService) GetStatus(*general.GetStatusParams, ...general.ClientOption) (*general.GetStatusOK, error) {
if m.Err != nil {
return nil, m.Err
}
return m.OkResponse, nil
}

func (m *mockClientService) SetTransport(runtime.ClientTransport) {}

func ptr[T any](t T) *T { return &t }
4 changes: 2 additions & 2 deletions internal/saauth/saauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ func Test_ServiceAccountAuthInfoWriter_AuthenticateRequest(t *testing.T) {
require.NoError(t, os.WriteFile(tokenFile, []byte("new-token"), 0644))
require.EventuallyWithT(t, func(t *assert.CollectT) {
r := new(runtime.TestClientRequest)
require.NoError(t, subject.AuthenticateRequest(r, nil))
require.Equal(t, "Bearer new-token", r.GetHeaderParams().Get("Authorization"))
assert.NoError(t, subject.AuthenticateRequest(r, nil))
assert.Equal(t, "Bearer new-token", r.GetHeaderParams().Get("Authorization"))
}, 5*time.Second, time.Millisecond)
}

Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"

alertscollector "github.com/appuio/alerts_exporter/internal/alerts_collector"
"github.com/appuio/alerts_exporter/internal/healthcheck"
"github.com/appuio/alerts_exporter/internal/saauth"
openapiclient "github.com/go-openapi/runtime/client"
alertmanagerclient "github.com/prometheus/alertmanager/api/v2/client"
Expand Down Expand Up @@ -100,6 +101,7 @@ func main() {
// Expose metrics and custom registry via an HTTP server
// using the HandleFor function. "/metrics" is the usual endpoint for that.
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
http.HandleFunc("/healthz", healthcheck.HealthCheck{GeneralService: ac.General}.HandleHealthz)
log.Printf("Listening on `%s`", listenAddr)
log.Fatal(http.ListenAndServe(listenAddr, nil))
}
Expand Down

0 comments on commit bd9a972

Please sign in to comment.