Skip to content

Commit

Permalink
Add instrumentation for net/http and net/httptrace (#190)
Browse files Browse the repository at this point in the history
* Move othttp instrumentation to contrib repo

* api/standard package has moved to semconv

* Replace othttp references with otelhttp

* Revert "api/standard package has moved to semconv"

This reverts commit eceaa35 as the change has not yet
been published in a versioned module.  Leaving the revert commit in history
for ease of resurrection.

* reference correct contrib module version

* Add net/http/httptrace instrumentation

* add httptrace module to dependabot config

* fix precommit issues

* Add net/http example

* add httptrace example

* Restore response writer wrapper from opentelemery-go#979

* Add CHANGELOG entry

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
  • Loading branch information
Aneurysm9 and MrAlias authored Aug 6, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent b469fe2 commit 3fc65dc
Showing 47 changed files with 3,834 additions and 2 deletions.
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -65,3 +65,11 @@ updates:
directory: "/instrumentation/github.com/Shopify/sarama" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/instrumentation/net/http" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/instrumentation/net/http/httptrace" # Location of package manifests
schedule:
interval: "daily"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -8,5 +8,6 @@ Thumbs.db
*.so
coverage.*

instrumentation/github.com/gocql/gocql/example/example
instrumentation/google.golang.org/grpc/example/server/server
instrumentation/google.golang.org/grpc/example/client/client
instrumentation/google.golang.org/grpc/example/client/client
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added

- The `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc` module has been added to replace the instrumentation that had previoiusly existed in the `go.opentelemetry.io/otel/instrumentation/grpctrace` package. (#189)
- Instrumentation for the stdlib `net/http` and `net/http/httptrace` packages. (#190)

## [0.10.0] - 2020-07-31

2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Instrumentation

Code contained in this directory contains instrumentation for 3rd-party Go packages.
Code contained in this directory contains instrumentation for 3rd-party Go packages and some packages from the standard library.

## Organization

41 changes: 41 additions & 0 deletions instrumentation/net/http/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 (
"net/http"

"go.opentelemetry.io/otel/api/kv"
)

// Attribute keys that can be added to a span.
const (
ReadBytesKey = kv.Key("http.read_bytes") // if anything was read from the request body, the total number of bytes read
ReadErrorKey = kv.Key("http.read_error") // If an error occurred while reading a request, the string of the error (io.EOF is not recorded)
WroteBytesKey = kv.Key("http.wrote_bytes") // if anything was written to the response writer, the total number of bytes written
WriteErrorKey = kv.Key("http.write_error") // if an error occurred while writing a reply, the string of the error (io.EOF is not recorded)
)

// Server HTTP metrics
const (
RequestCount = "http.server.request_count" // Incoming request count total
RequestContentLength = "http.server.request_content_length" // Incoming request bytes total
ResponseContentLength = "http.server.response_content_length" // Incoming response bytes total
ServerLatency = "http.server.duration" // Incoming end to end duration, microseconds
)

// Filter is a predicate used to determine whether a given http.request should
// be traced. A Filter must return true if the request should be traced.
type Filter func(*http.Request) bool
150 changes: 150 additions & 0 deletions instrumentation/net/http/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 (
"net/http"

"go.opentelemetry.io/otel/api/metric"
"go.opentelemetry.io/otel/api/propagation"
"go.opentelemetry.io/otel/api/trace"
)

// Config represents the configuration options available for the http.Handler
// and http.Transport types.
type Config struct {
Tracer trace.Tracer
Meter metric.Meter
Propagators propagation.Propagators
SpanStartOptions []trace.StartOption
ReadEvent bool
WriteEvent bool
Filters []Filter
SpanNameFormatter func(string, *http.Request) string
}

// Option Interface used for setting *optional* Config properties
type Option interface {
Apply(*Config)
}

// OptionFunc provides a convenience wrapper for simple Options
// that can be represented as functions.
type OptionFunc func(*Config)

func (o OptionFunc) Apply(c *Config) {
o(c)
}

// NewConfig creates a new Config struct and applies opts to it.
func NewConfig(opts ...Option) *Config {
c := &Config{}
for _, opt := range opts {
opt.Apply(c)
}
return c
}

// WithTracer configures a specific tracer. If this option
// isn't specified then the global tracer is used.
func WithTracer(tracer trace.Tracer) Option {
return OptionFunc(func(c *Config) {
c.Tracer = tracer
})
}

// WithMeter configures a specific meter. If this option
// isn't specified then the global meter is used.
func WithMeter(meter metric.Meter) Option {
return OptionFunc(func(c *Config) {
c.Meter = meter
})
}

// WithPublicEndpoint configures the Handler to link the span with an incoming
// span context. If this option is not provided, then the association is a child
// association instead of a link.
func WithPublicEndpoint() Option {
return OptionFunc(func(c *Config) {
c.SpanStartOptions = append(c.SpanStartOptions, trace.WithNewRoot())
})
}

// WithPropagators configures specific propagators. If this
// option isn't specified then
// go.opentelemetry.io/otel/api/global.Propagators are used.
func WithPropagators(ps propagation.Propagators) Option {
return OptionFunc(func(c *Config) {
c.Propagators = ps
})
}

// WithSpanOptions configures an additional set of
// trace.StartOptions, which are applied to each new span.
func WithSpanOptions(opts ...trace.StartOption) Option {
return OptionFunc(func(c *Config) {
c.SpanStartOptions = append(c.SpanStartOptions, opts...)
})
}

// WithFilter adds a filter to the list of filters used by the handler.
// If any filter indicates to exclude a request then the request will not be
// traced. All filters must allow a request to be traced for a Span to be created.
// If no filters are provided then all requests are traced.
// Filters will be invoked for each processed request, it is advised to make them
// simple and fast.
func WithFilter(f Filter) Option {
return OptionFunc(func(c *Config) {
c.Filters = append(c.Filters, f)
})
}

type event int

// Different types of events that can be recorded, see WithMessageEvents
const (
ReadEvents event = iota
WriteEvents
)

// WithMessageEvents configures the Handler to record the specified events
// (span.AddEvent) on spans. By default only summary attributes are added at the
// end of the request.
//
// Valid events are:
// * ReadEvents: Record the number of bytes read after every http.Request.Body.Read
// using the ReadBytesKey
// * WriteEvents: Record the number of bytes written after every http.ResponeWriter.Write
// using the WriteBytesKey
func WithMessageEvents(events ...event) Option {
return OptionFunc(func(c *Config) {
for _, e := range events {
switch e {
case ReadEvents:
c.ReadEvent = true
case WriteEvents:
c.WriteEvent = true
}
}
})
}

// WithSpanNameFormatter takes a function that will be called on every
// request and the returned string will become the Span Name
func WithSpanNameFormatter(f func(operation string, r *http.Request) string) Option {
return OptionFunc(func(c *Config) {
c.SpanNameFormatter = f
})
}
128 changes: 128 additions & 0 deletions instrumentation/net/http/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 (
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

mocktrace "go.opentelemetry.io/contrib/internal/trace"
)

func TestBasicFilter(t *testing.T) {
rr := httptest.NewRecorder()

tracer := mocktrace.Tracer{}

h := NewHandler(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := io.WriteString(w, "hello world"); err != nil {
t.Fatal(err)
}
}), "test_handler",
WithTracer(&tracer),
WithFilter(func(r *http.Request) bool {
return false
}),
)

r, err := http.NewRequest(http.MethodGet, "http://localhost/", nil)
if err != nil {
t.Fatal(err)
}
h.ServeHTTP(rr, r)
if got, expected := rr.Result().StatusCode, http.StatusOK; got != expected {
t.Fatalf("got %d, expected %d", got, expected)
}
if got := rr.Header().Get("Traceparent"); got != "" {
t.Fatal("expected empty trace header")
}
if got, expected := tracer.StartSpanID, uint64(0); got != expected {
t.Fatalf("got %d, expected %d", got, expected)
}
d, err := ioutil.ReadAll(rr.Result().Body)
if err != nil {
t.Fatal(err)
}
if got, expected := string(d), "hello world"; got != expected {
t.Fatalf("got %q, expected %q", got, expected)
}
}

func TestSpanNameFormatter(t *testing.T) {
var testCases = []struct {
name string
formatter func(s string, r *http.Request) string
operation string
expected string
}{
{
name: "default handler formatter",
formatter: defaultHandlerFormatter,
operation: "test_operation",
expected: "test_operation",
},
{
name: "default transport formatter",
formatter: defaultTransportFormatter,
expected: http.MethodGet,
},
{
name: "custom formatter",
formatter: func(s string, r *http.Request) string {
return r.URL.Path
},
operation: "",
expected: "/hello",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rr := httptest.NewRecorder()
var spanName string
tracer := mocktrace.Tracer{
OnSpanStarted: func(span *mocktrace.Span) {
spanName = span.Name
},
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := io.WriteString(w, "hello world"); err != nil {
t.Fatal(err)
}
})
h := NewHandler(
handler,
tc.operation,
WithTracer(&tracer),
WithSpanNameFormatter(tc.formatter),
)
r, err := http.NewRequest(http.MethodGet, "http://localhost/hello", nil)
if err != nil {
t.Fatal(err)
}
h.ServeHTTP(rr, r)
if got, expected := rr.Result().StatusCode, http.StatusOK; got != expected {
t.Fatalf("got %d, expected %d", got, expected)
}
if got, expected := spanName, tc.expected; got != expected {
t.Fatalf("got %q, expected %q", got, expected)
}
})
}
}
18 changes: 18 additions & 0 deletions instrumentation/net/http/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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 provides a http.Handler and functions that are
// intended to be used to add tracing by wrapping
// existing handlers (with Handler) and routes WithRouteTag.
package http
Loading

0 comments on commit 3fc65dc

Please sign in to comment.