Skip to content

Commit

Permalink
Add endpoint metrics for prometheus.
Browse files Browse the repository at this point in the history
  • Loading branch information
mcdee committed Dec 7, 2023
1 parent 3087274 commit a05485e
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 5 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
dev:
0.19.7:
- add endpoint metrics for prometheus

0.19.5:
- standardise names of options
- add common options (currently just timeout) to options structs

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func main() {
// supported by all clients, so checks should be made for each function when
// casting the service to the relevant interface.
if provider, isProvider := client.(eth2client.GenesisProvider); isProvider {
genesisResponse, err := provider.Genesis(ctx)
genesisResponse, err := provider.Genesis(ctx, &api.GenesisOpts{})
if err != nil {
// Errors may be API errors, in which case they will have more detail
// about the failure.
Expand All @@ -97,7 +97,7 @@ func main() {

// You can also access the struct directly if required.
httpClient := client.(*http.Service)
genesisResponse, err := httpClient.Genesis(ctx)
genesisResponse, err := httpClient.Genesis(ctx, &api.GenesisOpts{})
if err != nil {
panic(err)
}
Expand Down
15 changes: 13 additions & 2 deletions http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ func (s *Service) post(ctx context.Context, endpoint string, body io.Reader) (io
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "go-eth2-client/0.19.6")
req.Header.Set("User-Agent", "go-eth2-client/0.19.7")
}

resp, err := s.client.Do(req)
if err != nil {
cancel()
s.monitorPostComplete(ctx, url.Path, "failed")

return nil, errors.Wrap(err, "failed to call POST endpoint")
}
Expand All @@ -84,6 +85,7 @@ func (s *Service) post(ctx context.Context, endpoint string, body io.Reader) (io
if statusFamily != 2 {
cancel()
log.Trace().Int("status_code", resp.StatusCode).Str("data", string(data)).Msg("POST failed")
s.monitorPostComplete(ctx, url.Path, "failed")

return nil, &api.Error{
Method: http.MethodPost,
Expand All @@ -95,6 +97,7 @@ func (s *Service) post(ctx context.Context, endpoint string, body io.Reader) (io
cancel()

log.Trace().Str("response", string(data)).Msg("POST response")
s.monitorPostComplete(ctx, url.Path, "succeeded")

return bytes.NewReader(data), nil
}
Expand Down Expand Up @@ -143,12 +146,13 @@ func (s *Service) post2(ctx context.Context,
req.Header.Set(k, v)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "go-eth2-client/0.19.6")
req.Header.Set("User-Agent", "go-eth2-client/0.19.7")
}

resp, err := s.client.Do(req)
if err != nil {
cancel()
s.monitorPostComplete(ctx, url.Path, "failed")

return nil, errors.Wrap(err, "failed to call POST endpoint")
}
Expand All @@ -165,6 +169,7 @@ func (s *Service) post2(ctx context.Context,
if statusFamily != 2 {
cancel()
log.Trace().Int("status_code", resp.StatusCode).Str("data", string(data)).Msg("POST failed")
s.monitorPostComplete(ctx, url.Path, "failed")

return nil, &api.Error{
Method: http.MethodPost,
Expand All @@ -176,6 +181,7 @@ func (s *Service) post2(ctx context.Context,
cancel()

log.Trace().Str("response", string(data)).Msg("POST response")
s.monitorPostComplete(ctx, url.Path, "succeeded")

return bytes.NewReader(data), nil
}
Expand Down Expand Up @@ -240,6 +246,7 @@ func (s *Service) get(ctx context.Context, endpoint string, opts *api.CommonOpts
resp, err := s.client.Do(req)
if err != nil {
span.RecordError(errors.New("Request failed"))
s.monitorGetComplete(ctx, url.Path, "failed")

return nil, errors.Wrap(err, "failed to call GET endpoint")
}
Expand All @@ -255,6 +262,7 @@ func (s *Service) get(ctx context.Context, endpoint string, opts *api.CommonOpts
// Nothing returned. This is not considered an error.
span.AddEvent("Received empty response")
log.Trace().Msg("Endpoint returned no content")
s.monitorGetComplete(ctx, url.Path, "failed")

return res, nil
}
Expand All @@ -276,6 +284,7 @@ func (s *Service) get(ctx context.Context, endpoint string, opts *api.CommonOpts
span.SetStatus(codes.Error, fmt.Sprintf("Status code %d", resp.StatusCode))
trimmedResponse := bytes.ReplaceAll(bytes.ReplaceAll(res.body, []byte{0x0a}, []byte{}), []byte{0x0d}, []byte{})
log.Debug().Int("status_code", resp.StatusCode).RawJSON("response", trimmedResponse).Msg("GET failed")
s.monitorGetComplete(ctx, url.Path, "failed")

return nil, &api.Error{
Method: http.MethodGet,
Expand All @@ -296,6 +305,8 @@ func (s *Service) get(ctx context.Context, endpoint string, opts *api.CommonOpts
return nil, errors.Wrap(err, "failed to parse consensus version")
}

s.monitorGetComplete(ctx, url.Path, "succeeded")

return res, nil
}

Expand Down
113 changes: 113 additions & 0 deletions http/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright © 2023 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import (
"context"
"regexp"

"github.com/attestantio/go-eth2-client/metrics"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
)

var requestsMetric *prometheus.CounterVec

func registerMetrics(ctx context.Context, monitor metrics.Service) error {
if requestsMetric != nil {
// Already registered.
return nil
}
if monitor == nil {
// No monitor.
return nil
}
if monitor.Presenter() == "prometheus" {
return registerPrometheusMetrics(ctx)
}

return nil
}

func registerPrometheusMetrics(_ context.Context) error {
requestsMetric = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "consensusclient",
Subsystem: "http",
Name: "requests_total",
Help: "Number of requests",
}, []string{"server", "method", "endpoint", "result"})
if err := prometheus.Register(requestsMetric); err != nil {
return errors.Wrap(err, "failed to register requests_total")
}

return nil
}

func (s *Service) monitorGetComplete(_ context.Context, endpoint string, result string) {
if requestsMetric != nil {
requestsMetric.WithLabelValues(s.address, "GET", reduceEndpoint(endpoint), result).Inc()
}
}

func (s *Service) monitorPostComplete(_ context.Context, endpoint string, result string) {
if requestsMetric != nil {
requestsMetric.WithLabelValues(s.address, "POST", reduceEndpoint(endpoint), result).Inc()
}
}

type templateReplacement struct {
pattern *regexp.Regexp
replacement []byte
}

var endpointTemplates = []*templateReplacement{
{
pattern: regexp.MustCompile("/(blinded_blocks|blob_sidecars|blocks|headers|sync_committee)/(0x[0-9a-fA-F]{64}|[0-9]+|head|genesis|finalized)"),
replacement: []byte("/$1/{block_id}"),
},
{
pattern: regexp.MustCompile("/bootstrap/0x[0-9a-fA-F]{64}"),
replacement: []byte("/bootstrap/{block_root}"),
},
{
pattern: regexp.MustCompile("/duties/(attester|proposer|sync)/[0-9]+"),
replacement: []byte("/duties/$1/{epoch}"),
},
{
pattern: regexp.MustCompile("/peers/[0-9a-zA-Z]+"),
replacement: []byte("/peers/{peer_id}"),
},
{
pattern: regexp.MustCompile("/rewards/attestations/[0-9]+"),
replacement: []byte("/rewards/attestations/{epoch}"),
},
{
pattern: regexp.MustCompile("/states/(0x[0-9a-fA-F]{64}|[0-9]+|head|genesis|finalized)"),
replacement: []byte("/states/{state_id}"),
},
{
pattern: regexp.MustCompile("/validators/(0x[0-9a-fA-F]{64}|[0-9]+)"),
replacement: []byte("/validators/{validator_id}"),
},
}

// reduceEndpoint reduces an endpoint to its template.
func reduceEndpoint(in string) string {
out := []byte(in)
for _, template := range endpointTemplates {
out = template.pattern.ReplaceAll(out, template.replacement)
}

return string(out)
}
9 changes: 9 additions & 0 deletions http/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ package http
import (
"time"

"github.com/attestantio/go-eth2-client/metrics"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)

type parameters struct {
logLevel zerolog.Level
monitor metrics.Service
address string
timeout time.Duration
indexChunkSize int
Expand All @@ -48,6 +50,13 @@ func WithLogLevel(logLevel zerolog.Level) Parameter {
})
}

// WithMonitor sets the monitor for the service.
func WithMonitor(monitor metrics.Service) Parameter {
return parameterFunc(func(p *parameters) {
p.monitor = monitor
})
}

// WithAddress provides the address for the endpoint.
func WithAddress(address string) Parameter {
return parameterFunc(func(p *parameters) {
Expand Down
6 changes: 6 additions & 0 deletions http/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ func New(ctx context.Context, params ...Parameter) (eth2client.Service, error) {
log = log.Level(parameters.logLevel)
}

if parameters.monitor != nil {
if err := registerMetrics(ctx, parameters.monitor); err != nil {
return nil, errors.Wrap(err, "failed to register metrics")
}
}

client := &http.Client{
Timeout: parameters.timeout,
Transport: &http.Transport{
Expand Down

0 comments on commit a05485e

Please sign in to comment.