Skip to content
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

Add /healthz endpoint that checks Alertmanager connection #25

Merged
merged 3 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading