mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-04 01:09:36 +02:00
healthcheck: only report transitions (#5068)
This commit is contained in:
parent
1aa062b37b
commit
deb6f67094
5 changed files with 207 additions and 5 deletions
80
pkg/health/deduplicate.go
Normal file
80
pkg/health/deduplicate.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
var _ Provider = (*deduplicator)(nil)
|
||||
|
||||
// deduplicator is a health check provider that deduplicates health check reports
|
||||
// i.e. it only reports a health check if the status or attributes have changed
|
||||
type deduplicator struct {
|
||||
seen sync.Map
|
||||
provider Provider
|
||||
}
|
||||
|
||||
type record struct {
|
||||
attr map[string]string
|
||||
err *string
|
||||
}
|
||||
|
||||
func newOKRecord(attrs []Attr) *record {
|
||||
return newRecord(nil, attrs)
|
||||
}
|
||||
|
||||
func newErrorRecord(err error, attrs []Attr) *record {
|
||||
errTxt := err.Error()
|
||||
return newRecord(&errTxt, attrs)
|
||||
}
|
||||
|
||||
func newRecord(err *string, attrs []Attr) *record {
|
||||
r := &record{err: err, attr: make(map[string]string)}
|
||||
for _, a := range attrs {
|
||||
r.attr[a.Key] = a.Value
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *record) Equals(other *record) bool {
|
||||
return r.equalError(other) &&
|
||||
maps.Equal(r.attr, other.attr)
|
||||
}
|
||||
|
||||
func (r *record) equalError(other *record) bool {
|
||||
if r.err == nil || other.err == nil {
|
||||
return r.err == other.err
|
||||
}
|
||||
return *r.err == *other.err
|
||||
}
|
||||
|
||||
func NewDeduplicator(provider Provider) Provider {
|
||||
return &deduplicator{provider: provider}
|
||||
}
|
||||
|
||||
func (d *deduplicator) swap(check Check, next *record) *record {
|
||||
prev, there := d.seen.Swap(check, next)
|
||||
if !there {
|
||||
return nil
|
||||
}
|
||||
return prev.(*record)
|
||||
}
|
||||
|
||||
// ReportError implements the Provider interface
|
||||
func (d *deduplicator) ReportError(check Check, err error, attrs ...Attr) {
|
||||
cur := newErrorRecord(err, attrs)
|
||||
prev := d.swap(check, cur)
|
||||
if prev == nil || !cur.Equals(prev) {
|
||||
d.provider.ReportError(check, err, attrs...)
|
||||
}
|
||||
}
|
||||
|
||||
// ReportOK implements the Provider interface
|
||||
func (d *deduplicator) ReportOK(check Check, attrs ...Attr) {
|
||||
cur := newOKRecord(attrs)
|
||||
prev := d.swap(check, cur)
|
||||
if prev == nil || !cur.Equals(prev) {
|
||||
d.provider.ReportOK(check, attrs...)
|
||||
}
|
||||
}
|
53
pkg/health/deduplicate_test.go
Normal file
53
pkg/health/deduplicate_test.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package health_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
|
||||
health "github.com/pomerium/pomerium/pkg/health"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/golang/mock/mockgen -package health_test -destination provider_mock_test.go github.com/pomerium/pomerium/pkg/health Provider
|
||||
|
||||
func TestDeduplicate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewMockProvider(gomock.NewController(t))
|
||||
dp := health.NewDeduplicator(p)
|
||||
|
||||
check1, check2 := health.Check("check-1"), health.Check("check-2")
|
||||
p.EXPECT().ReportOK(check1).Times(1)
|
||||
p.EXPECT().ReportOK(check2).Times(1)
|
||||
dp.ReportOK(check1)
|
||||
dp.ReportOK(check2)
|
||||
dp.ReportOK(check1)
|
||||
|
||||
p.EXPECT().ReportError(check1, gomock.Any()).Times(1)
|
||||
dp.ReportError(check1, errors.New("error"))
|
||||
dp.ReportError(check1, errors.New("error"))
|
||||
|
||||
p.EXPECT().ReportOK(check1).Times(1)
|
||||
dp.ReportOK(check1)
|
||||
|
||||
p.EXPECT().ReportOK(check1, health.StrAttr("k1", "v1")).Times(2)
|
||||
p.EXPECT().ReportOK(check1, health.StrAttr("k1", "v2")).Times(1)
|
||||
dp.ReportOK(check1, health.StrAttr("k1", "v1"))
|
||||
dp.ReportOK(check1, health.StrAttr("k1", "v2"))
|
||||
dp.ReportOK(check1, health.StrAttr("k1", "v1"))
|
||||
}
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := NewMockProvider(gomock.NewController(t))
|
||||
health.SetProvider(p)
|
||||
|
||||
check1 := health.Check("check-1")
|
||||
p.EXPECT().ReportOK(check1).Times(1)
|
||||
health.ReportOK(check1)
|
||||
|
||||
health.SetProvider(nil)
|
||||
health.ReportOK(check1)
|
||||
}
|
|
@ -39,6 +39,9 @@ type Provider interface {
|
|||
|
||||
// SetProvider sets the health check provider
|
||||
func SetProvider(p Provider) {
|
||||
if p != nil {
|
||||
p = NewDeduplicator(p)
|
||||
}
|
||||
defaultProvider.Store(p)
|
||||
}
|
||||
|
||||
|
|
69
pkg/health/provider_mock_test.go
Normal file
69
pkg/health/provider_mock_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/pomerium/pomerium/pkg/health (interfaces: Provider)
|
||||
|
||||
// Package health_test is a generated GoMock package.
|
||||
package health_test
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
health "github.com/pomerium/pomerium/pkg/health"
|
||||
)
|
||||
|
||||
// MockProvider is a mock of Provider interface.
|
||||
type MockProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockProviderMockRecorder is the mock recorder for MockProvider.
|
||||
type MockProviderMockRecorder struct {
|
||||
mock *MockProvider
|
||||
}
|
||||
|
||||
// NewMockProvider creates a new mock instance.
|
||||
func NewMockProvider(ctrl *gomock.Controller) *MockProvider {
|
||||
mock := &MockProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockProvider) EXPECT() *MockProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ReportError mocks base method.
|
||||
func (m *MockProvider) ReportError(arg0 health.Check, arg1 error, arg2 ...health.Attr) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "ReportError", varargs...)
|
||||
}
|
||||
|
||||
// ReportError indicates an expected call of ReportError.
|
||||
func (mr *MockProviderMockRecorder) ReportError(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportError", reflect.TypeOf((*MockProvider)(nil).ReportError), varargs...)
|
||||
}
|
||||
|
||||
// ReportOK mocks base method.
|
||||
func (m *MockProvider) ReportOK(arg0 health.Check, arg1 ...health.Attr) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
m.ctrl.Call(m, "ReportOK", varargs...)
|
||||
}
|
||||
|
||||
// ReportOK indicates an expected call of ReportOK.
|
||||
func (mr *MockProviderMockRecorder) ReportOK(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReportOK", reflect.TypeOf((*MockProvider)(nil).ReportOK), varargs...)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue