Skip to content
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 instrumentation for net/http and net/httptrace #190

Merged
merged 14 commits into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,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"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Thumbs.db
*.so
coverage.*

instrumentation/github.com/gocql/gocql/example/example
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
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

Expand Down
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
MrAlias marked this conversation as resolved.
Show resolved Hide resolved
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
24 changes: 24 additions & 0 deletions instrumentation/net/http/example/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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.
FROM golang:1.14-alpine AS base
COPY . /src/
WORKDIR /src/instrumentation/net/http/example

FROM base AS example-http-server
RUN go install ./server/server.go
CMD ["/go/bin/server"]

FROM base AS example-http-client
RUN go install ./client/client.go
CMD ["/go/bin/client"]
Loading