Skip to content

Commit

Permalink
Allow custom labels to be added to net/http metrics (#306)
Browse files Browse the repository at this point in the history
* Allow custom labels to be added to net/http metrics

* CHANGELOG entry

* Add license to labeler.go

* Address PR Feedback:

* Add mutex around access to labels
* Add Labeler use to handler example test

* Add getter to Labeler
  • Loading branch information
Aneurysm9 authored Sep 9, 2020
1 parent 5a0832b commit c55a925
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

- Benchmark tests for the gRPC instrumentation. (#296)
- Integration testing for the gRPC instrumentation. (#297)
- Allow custom labels to be added to net/http metrics. (#306)

### Changed

Expand Down
5 changes: 4 additions & 1 deletion instrumentation/net/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
},
})

labeler := &Labeler{}
ctx = injectLabeler(ctx, labeler)

h.handler.ServeHTTP(w, r.WithContext(ctx))

setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err)

// Add request metrics

labels := semconv.HTTPServerMetricAttributesFromHTTPRequest(h.operation, r)
labels := append(labeler.Get(), semconv.HTTPServerMetricAttributesFromHTTPRequest(h.operation, r)...)

h.counters[RequestContentLength].Add(ctx, bw.read, labels...)
h.counters[ResponseContentLength].Add(ctx, rww.written, labels...)
Expand Down
5 changes: 5 additions & 0 deletions instrumentation/net/http/handler_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func ExampleNewHandler() {
otelhttp.WithRouteTag("/hello/:name", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
labeler, _ := otelhttp.LabelerFromContext(ctx)

var name string
// Wrap another function in its own span
if err := func(ctx context.Context) error {
Expand All @@ -72,19 +74,22 @@ func ExampleNewHandler() {
}(ctx); err != nil {
log.Println("error figuring out name: ", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
labeler.Add(label.Bool("error", true))
return
}

d, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Println("error reading body: ", err)
w.WriteHeader(http.StatusBadRequest)
labeler.Add(label.Bool("error", true))
return
}

n, err := io.WriteString(w, "Hello, "+name+"!\nYou sent me this:\n"+string(d))
if err != nil {
log.Printf("error writing reply after %d bytes: %s", n, err)
labeler.Add(label.Bool("error", true))
}
}),
),
Expand Down
4 changes: 4 additions & 0 deletions instrumentation/net/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ func TestHandlerBasics(t *testing.T) {

h := NewHandler(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l, _ := LabelerFromContext(r.Context())
l.Add(label.String("test", "label"))

if _, err := io.WriteString(w, "hello world"); err != nil {
t.Fatal(err)
}
Expand All @@ -73,6 +76,7 @@ func TestHandlerBasics(t *testing.T) {
semconv.HTTPSchemeHTTP,
semconv.HTTPHostKey.String(r.Host),
semconv.HTTPFlavorKey.String(fmt.Sprintf("1.%d", r.ProtoMinor)),
label.String("test", "label"),
}

assertMetricLabels(t, labelsToVerify, meterimpl.MeasurementBatches)
Expand Down
65 changes: 65 additions & 0 deletions instrumentation/net/http/labeler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright The OpenTelemetry Authors
//
// 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"
"sync"

"go.opentelemetry.io/otel/label"
)

// Labeler is used to allow instrumented HTTP handlers to add custom labels to
// the metrics recorded by the net/http instrumentation.
type Labeler struct {
mu sync.Mutex
labels []label.KeyValue
}

// Add labels to a Labeler.
func (l *Labeler) Add(ls ...label.KeyValue) {
l.mu.Lock()
defer l.mu.Unlock()
l.labels = append(l.labels, ls...)
}

// Labels returns a copy of the labels added to the Labeler.
func (l *Labeler) Get() []label.KeyValue {
l.mu.Lock()
defer l.mu.Unlock()
ret := make([]label.KeyValue, len(l.labels))
copy(ret, l.labels)
return ret
}

type labelerContextKeyType int

const lablelerContextKey labelerContextKeyType = 0

func injectLabeler(ctx context.Context, l *Labeler) context.Context {
return context.WithValue(ctx, lablelerContextKey, l)
}

// LabelerFromContext retrieves a Labeler instance from the provided context if
// one is available. If no Labeler was found in the provided context a new, empty
// Labeler is returned and the second return value is false. In this case it is
// safe to use the Labeler but any labels added to it will not be used.
func LabelerFromContext(ctx context.Context) (*Labeler, bool) {
l, ok := ctx.Value(lablelerContextKey).(*Labeler)
if !ok {
l = &Labeler{}
}
return l, ok
}

0 comments on commit c55a925

Please sign in to comment.