Skip to content

Commit d8a2c92

Browse files
authored
Add error callbacks that emit metrics (#53)
These are non-default because they require a metrics registry, but the default handlers are now implemented by passing a nil registry to the metric version. Note that this contains an API break in the AsyncErrorCallback type, but I don't expect there to be many (any?) custom implementations.
1 parent f400e7c commit d8a2c92

File tree

5 files changed

+94
-25
lines changed

5 files changed

+94
-25
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,11 @@ baseHandler, err := githubapp.NewDefaultCachingClientCreator(
205205

206206
## Metrics
207207

208-
`go-githubapp` uses [rcrowley/go-metrics][] to provide metrics. GitHub clients
209-
emit the metrics below if configured with the `githubapp.ClientMetrics`
210-
middleware.
208+
`go-githubapp` uses [rcrowley/go-metrics][] to provide metrics. Metrics are
209+
optional and disabled by default.
210+
211+
GitHub clients emit the following metrics when configured with the
212+
`githubapp.ClientMetrics` middleware:
211213

212214
| metric name | type | definition |
213215
| ----------- | ---- | ---------- |
@@ -230,6 +232,13 @@ When using [asynchronous dispatch](#asynchronous-dispatch), the
230232
| `github.event.dropped` | `counter` | the number events dropped due to limited queue capacity |
231233
| `github.event.age` | `histogram` | the age (queue time) in milliseconds of events at processing time |
232234

235+
The `MetricsErrorCallback` and `MetricsAsyncErrorCallback` error callbacks for
236+
the event dispatcher and asynchronous schedulers emit the following metrics:
237+
238+
| metric name | type | definition |
239+
| ----------- | ---- | ---------- |
240+
| `github.handler.error[event:<type>]` | `counter` | the number of processing errors, tagged with the GitHub event type |
241+
233242
Note that metrics need to be published in order to be useful. Several
234243
[publishing options][] are available or you can implement your own.
235244

githubapp/dispatcher.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/google/go-github/v32/github"
2323
"github.com/pkg/errors"
24+
"github.com/rcrowley/go-metrics"
2425
"github.com/rs/zerolog"
2526
)
2627

@@ -201,24 +202,36 @@ func (d *eventDispatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
201202
d.onResponse(w, r, eventType, ok)
202203
}
203204

204-
// DefaultErrorCallback logs errors and responds with a 500 status code.
205+
// DefaultErrorCallback logs errors and responds with an appropriate status code.
205206
func DefaultErrorCallback(w http.ResponseWriter, r *http.Request, err error) {
206-
logger := zerolog.Ctx(r.Context())
207+
defaultErrorCallback(w, r, err)
208+
}
207209

208-
var ve ValidationError
209-
if errors.As(err, &ve) {
210-
logger.Warn().Err(ve.Cause).Msgf("Received invalid webhook headers or payload")
211-
http.Error(w, "Invalid webhook headers or payload", http.StatusBadRequest)
212-
return
213-
}
214-
if errors.Is(err, ErrCapacityExceeded) {
215-
logger.Warn().Msg("Dropping webhook event due to over-capacity scheduler")
216-
http.Error(w, "No capacity available to processes this event", http.StatusServiceUnavailable)
217-
return
218-
}
210+
var defaultErrorCallback = MetricsErrorCallback(nil)
211+
212+
// MetricsErrorCallback logs errors, increments an error counter, and responds
213+
// with an appropriate status code.
214+
func MetricsErrorCallback(reg metrics.Registry) ErrorCallback {
215+
return func(w http.ResponseWriter, r *http.Request, err error) {
216+
logger := zerolog.Ctx(r.Context())
219217

220-
logger.Error().Err(err).Msg("Unexpected error handling webhook")
221-
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
218+
var ve ValidationError
219+
if errors.As(err, &ve) {
220+
logger.Warn().Err(ve.Cause).Msgf("Received invalid webhook headers or payload")
221+
http.Error(w, "Invalid webhook headers or payload", http.StatusBadRequest)
222+
return
223+
}
224+
if errors.Is(err, ErrCapacityExceeded) {
225+
logger.Warn().Msg("Dropping webhook event due to over-capacity scheduler")
226+
http.Error(w, "No capacity available to processes this event", http.StatusServiceUnavailable)
227+
return
228+
}
229+
230+
logger.Error().Err(err).Msg("Unexpected error handling webhook")
231+
errorCounter(reg, r.Header.Get("X-Github-Event")).Inc(1)
232+
233+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
234+
}
222235
}
223236

224237
// DefaultResponseCallback responds with a 200 OK for handled events and a 202

githubapp/errors.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2020 Palantir Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package githubapp
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/rcrowley/go-metrics"
21+
)
22+
23+
const (
24+
MetricsKeyHandlerError = "github.handler.error"
25+
)
26+
27+
func errorCounter(r metrics.Registry, event string) metrics.Counter {
28+
if r == nil {
29+
return metrics.NilCounter{}
30+
}
31+
32+
key := MetricsKeyHandlerError
33+
if event != "" {
34+
key = fmt.Sprintf("%s[event:%s]", key, event)
35+
}
36+
return metrics.GetOrRegisterCounter(key, r)
37+
}

githubapp/scheduler.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,21 @@ func (d Dispatch) Execute(ctx context.Context) error {
5959
// AsyncErrorCallback is called by an asynchronous scheduler when an event
6060
// handler returns an error. The error from the handler is passed directly as
6161
// the final argument.
62-
type AsyncErrorCallback func(ctx context.Context, err error)
62+
type AsyncErrorCallback func(ctx context.Context, d Dispatch, err error)
6363

6464
// DefaultAsyncErrorCallback logs errors.
65-
func DefaultAsyncErrorCallback(ctx context.Context, err error) {
66-
zerolog.Ctx(ctx).Error().Err(err).Msg("Unexpected error handling webhook")
65+
func DefaultAsyncErrorCallback(ctx context.Context, d Dispatch, err error) {
66+
defaultAsyncErrorCallback(ctx, d, err)
67+
}
68+
69+
var defaultAsyncErrorCallback = MetricsAsyncErrorCallback(nil)
70+
71+
// MetricsAsyncErrorCallback logs errors and increments an error counter.
72+
func MetricsAsyncErrorCallback(reg metrics.Registry) AsyncErrorCallback {
73+
return func(ctx context.Context, d Dispatch, err error) {
74+
zerolog.Ctx(ctx).Error().Err(err).Msg("Unexpected error handling webhook")
75+
errorCounter(reg, d.EventType).Inc(1)
76+
}
6777
}
6878

6979
// ContextDeriver creates a new independent context from a request's context.
@@ -156,6 +166,7 @@ type scheduler struct {
156166
func (s *scheduler) safeExecute(ctx context.Context, d Dispatch) {
157167
var err error
158168
defer func() {
169+
atomic.AddInt64(&s.activeWorkers, -1)
159170
if r := recover(); r != nil {
160171
if rerr, ok := r.(error); ok {
161172
err = rerr
@@ -164,9 +175,8 @@ func (s *scheduler) safeExecute(ctx context.Context, d Dispatch) {
164175
}
165176
}
166177
if err != nil && s.onError != nil {
167-
s.onError(ctx, err)
178+
s.onError(ctx, d, err)
168179
}
169-
atomic.AddInt64(&s.activeWorkers, -1)
170180
}()
171181

172182
atomic.AddInt64(&s.activeWorkers, 1)

githubapp/scheduler_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func TestAsyncScheduler(t *testing.T) {
6464

6565
t.Run("errorCallback", func(t *testing.T) {
6666
errc := make(chan error, 1)
67-
cb := func(ctx context.Context, err error) {
67+
cb := func(ctx context.Context, d Dispatch, err error) {
6868
errc <- err
6969
}
7070

@@ -115,7 +115,7 @@ func TestQueueAsyncScheduler(t *testing.T) {
115115

116116
t.Run("errorCallback", func(t *testing.T) {
117117
errc := make(chan error, 1)
118-
cb := func(ctx context.Context, err error) {
118+
cb := func(ctx context.Context, d Dispatch, err error) {
119119
errc <- err
120120
}
121121

0 commit comments

Comments
 (0)