-
Notifications
You must be signed in to change notification settings - Fork 159
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 support for Prometheus metric categories #539
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ import ( | |
"io" | ||
|
||
"github.com/buchgr/bazel-remote/cache" | ||
"google.golang.org/grpc/metadata" | ||
"net/http" | ||
|
||
pb "github.com/buchgr/bazel-remote/genproto/build/bazel/remote/execution/v2" | ||
|
||
|
@@ -14,15 +16,17 @@ import ( | |
type metricsDecorator struct { | ||
counter *prometheus.CounterVec | ||
*diskCache | ||
categories map[string][]string | ||
} | ||
|
||
const ( | ||
hitStatus = "hit" | ||
missStatus = "miss" | ||
hitStatus = "hit" | ||
missStatus = "miss" | ||
emptyStatus = "" | ||
|
||
containsMethod = "contains" | ||
getMethod = "get" | ||
//putMethod = "put" | ||
putMethod = "put" | ||
|
||
acKind = "ac" // This must be lowercase to match cache.EntryKind.String() | ||
casKind = "cas" | ||
|
@@ -46,6 +50,7 @@ func (m *metricsDecorator) Get(ctx context.Context, kind cache.EntryKind, hash s | |
} else { | ||
lbls["status"] = missStatus | ||
} | ||
m.addCategoryLabels(ctx, lbls) | ||
m.counter.With(lbls).Inc() | ||
|
||
return rc, size, nil | ||
|
@@ -63,6 +68,7 @@ func (m *metricsDecorator) GetValidatedActionResult(ctx context.Context, hash st | |
} else { | ||
lbls["status"] = missStatus | ||
} | ||
m.addCategoryLabels(ctx, lbls) | ||
m.counter.With(lbls).Inc() | ||
|
||
return ar, data, err | ||
|
@@ -83,6 +89,7 @@ func (m *metricsDecorator) GetZstd(ctx context.Context, hash string, size int64, | |
} else { | ||
lbls["status"] = missStatus | ||
} | ||
m.addCategoryLabels(ctx, lbls) | ||
m.counter.With(lbls).Inc() | ||
|
||
return rc, size, nil | ||
|
@@ -97,6 +104,7 @@ func (m *metricsDecorator) Contains(ctx context.Context, kind cache.EntryKind, h | |
} else { | ||
lbls["status"] = missStatus | ||
} | ||
m.addCategoryLabels(ctx, lbls) | ||
m.counter.With(lbls).Inc() | ||
|
||
return ok, size | ||
|
@@ -118,17 +126,97 @@ func (m *metricsDecorator) FindMissingCasBlobs(ctx context.Context, blobs []*pb. | |
"kind": "cas", | ||
"status": hitStatus, | ||
} | ||
m.addCategoryLabels(ctx, hitLabels) | ||
hits := m.counter.With(hitLabels) | ||
|
||
missLabels := prometheus.Labels{ | ||
"method": containsMethod, | ||
"kind": "cas", | ||
"status": missStatus, | ||
} | ||
m.addCategoryLabels(ctx, missLabels) | ||
misses := m.counter.With(missLabels) | ||
|
||
hits.Add(float64(numFound)) | ||
misses.Add(float64(numMissing)) | ||
|
||
return digests, nil | ||
} | ||
|
||
func (m *metricsDecorator) Put(ctx context.Context, kind cache.EntryKind, hash string, size int64, r io.Reader) error { | ||
err := m.diskCache.Put(ctx, kind, hash, size, r) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
lbls := prometheus.Labels{"method": putMethod, "kind": kind.String(), "status": emptyStatus} | ||
m.addCategoryLabels(ctx, lbls) | ||
m.counter.With(lbls).Inc() | ||
|
||
return nil | ||
} | ||
|
||
// Update prometheus labels based on HTTP and gRPC headers available via the context. | ||
func (m *metricsDecorator) addCategoryLabels(ctx context.Context, labels prometheus.Labels) { | ||
|
||
if len(m.categories) == 0 { | ||
return | ||
} | ||
|
||
httpHeaders := getHttpHeaders(ctx) | ||
var grpcHeaders metadata.MD | ||
if httpHeaders == nil { | ||
grpcHeaders = getGrpcHeaders(ctx) | ||
} | ||
|
||
for categoryNameLowerCase, allowedValues := range m.categories { | ||
// Lower case is canonical for gRPC headers and convention for prometheus. | ||
var headerValue string = "" | ||
if grpcHeaders != nil { | ||
grpcHeaderValues := grpcHeaders[categoryNameLowerCase] | ||
if len(grpcHeaderValues) > 0 { | ||
// Pick the first header with matching name if multiple headers with same name | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would be less surprising to pick the last matching header instead of the first? |
||
headerValue = grpcHeaderValues[0] | ||
} | ||
} else if httpHeaders != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just checking, when using grpc is it assumed that we should ignore http headers too? |
||
headerValue = httpHeaders.Get(categoryNameLowerCase) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: add an empty line after this |
||
if len(headerValue) == 0 { | ||
labels[categoryNameLowerCase] = "" | ||
} else if contains(allowedValues, headerValue) { | ||
labels[categoryNameLowerCase] = headerValue | ||
} else { | ||
labels[categoryNameLowerCase] = "other" | ||
} | ||
} | ||
} | ||
|
||
func contains(s []string, e string) bool { | ||
for _, a := range s { | ||
if a == e { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
type httpHeadersContextKey struct{} | ||
|
||
// Creates a context copy with HTTP headers attached. | ||
func ContextWithHttpHeaders(ctx context.Context, headers *http.Header) context.Context { | ||
return context.WithValue(ctx, httpHeadersContextKey{}, headers) | ||
} | ||
|
||
// Retrieves HTTP headers from context. Minimizes type safety issues. | ||
func getHttpHeaders(ctx context.Context) *http.Header { | ||
headers, ok := ctx.Value(httpHeadersContextKey{}).(*http.Header) | ||
if !ok { | ||
return nil | ||
} | ||
return headers | ||
} | ||
|
||
func getGrpcHeaders(ctx context.Context) metadata.MD { | ||
grpcHeaders, _ := metadata.FromIncomingContext(ctx) | ||
return grpcHeaders | ||
} | ||
Comment on lines
+210
to
+222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it would be a little cleaner to find http/grpc headers in the server package (separately in http and grpc code), and then add them as a single custom value in the context? Then we could simplify addCategoryLabels a little. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,6 +56,7 @@ type Config struct { | |
DisableGRPCACDepsCheck bool `yaml:"disable_grpc_ac_deps_check"` | ||
EnableACKeyInstanceMangling bool `yaml:"enable_ac_key_instance_mangling"` | ||
EnableEndpointMetrics bool `yaml:"enable_endpoint_metrics"` | ||
MetricCategories map[string][]string `yaml:"metric_categories"` | ||
MetricsDurationBuckets []float64 `yaml:"endpoint_metrics_duration_buckets"` | ||
ExperimentalRemoteAssetAPI bool `yaml:"experimental_remote_asset_api"` | ||
HTTPReadTimeout time.Duration `yaml:"http_read_timeout"` | ||
|
@@ -351,6 +352,12 @@ func validateConfig(c *Config) error { | |
return errors.New("'access_log_level' must be set to either \"none\" or \"all\"") | ||
} | ||
|
||
for categoryName := range c.MetricCategories { | ||
if categoryName != strings.ToLower(categoryName) { | ||
return fmt.Errorf("Names in 'metric_categories' must be in lower case: %s", categoryName) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: reword to |
||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should mention here that only lowercase category names are allowed, otherwise it will likely be a common error that people will hit when first trying this feature. Also: are non lowercase values allowed?