From 3fc65dc78f63bed02e186a9c22b7eb32b565fc56 Mon Sep 17 00:00:00 2001 From: Anthony Mirabella Date: Thu, 6 Aug 2020 12:20:34 -0400 Subject: [PATCH] Add instrumentation for net/http and net/httptrace (#190) * 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 eceaa351e438ff986af904fbeb40a9ff0eb79322 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 --- .github/dependabot.yml | 8 + .gitignore | 3 +- CHANGELOG.md | 1 + instrumentation/README.md | 2 +- instrumentation/net/http/common.go | 41 +++ instrumentation/net/http/config.go | 150 ++++++++++ instrumentation/net/http/config_test.go | 128 +++++++++ instrumentation/net/http/doc.go | 18 ++ instrumentation/net/http/example/Dockerfile | 24 ++ instrumentation/net/http/example/README.md | 24 ++ .../net/http/example/client/client.go | 94 +++++++ .../net/http/example/docker-compose.yml | 34 +++ instrumentation/net/http/example/go.mod | 15 + instrumentation/net/http/example/go.sum | 109 +++++++ .../net/http/example/server/modd.conf | 21 ++ .../net/http/example/server/server.go | 69 +++++ instrumentation/net/http/filters/filters.go | 128 +++++++++ .../net/http/filters/filters_test.go | 266 ++++++++++++++++++ .../net/http/filters/header_go14.go | 50 ++++ .../net/http/filters/header_nongo14.go | 51 ++++ instrumentation/net/http/go.mod | 13 + instrumentation/net/http/go.sum | 97 +++++++ instrumentation/net/http/handler.go | 221 +++++++++++++++ .../net/http/handler_example_test.go | 98 +++++++ instrumentation/net/http/handler_test.go | 164 +++++++++++ instrumentation/net/http/httptrace/api.go | 28 ++ .../net/http/httptrace/clienttrace.go | 253 +++++++++++++++++ .../net/http/httptrace/clienttrace_test.go | 238 ++++++++++++++++ .../net/http/httptrace/example/Dockerfile | 24 ++ .../net/http/httptrace/example/README.md | 24 ++ .../http/httptrace/example/client/client.go | 98 +++++++ .../http/httptrace/example/docker-compose.yml | 34 +++ .../net/http/httptrace/example/go.mod | 17 ++ .../net/http/httptrace/example/go.sum | 109 +++++++ .../http/httptrace/example/server/modd.conf | 21 ++ .../http/httptrace/example/server/server.go | 69 +++++ instrumentation/net/http/httptrace/go.mod | 12 + instrumentation/net/http/httptrace/go.sum | 94 +++++++ .../net/http/httptrace/httptrace.go | 74 +++++ .../net/http/httptrace/httptrace_test.go | 164 +++++++++++ instrumentation/net/http/transport.go | 134 +++++++++ .../net/http/transport_example_test.go | 27 ++ instrumentation/net/http/transport_test.go | 76 +++++ instrumentation/net/http/wrap.go | 96 +++++++ internal/metric/async.go | 148 ++++++++++ internal/metric/mock.go | 191 +++++++++++++ internal/metric/mock_test.go | 76 +++++ 47 files changed, 3834 insertions(+), 2 deletions(-) create mode 100644 instrumentation/net/http/common.go create mode 100644 instrumentation/net/http/config.go create mode 100644 instrumentation/net/http/config_test.go create mode 100644 instrumentation/net/http/doc.go create mode 100644 instrumentation/net/http/example/Dockerfile create mode 100644 instrumentation/net/http/example/README.md create mode 100644 instrumentation/net/http/example/client/client.go create mode 100644 instrumentation/net/http/example/docker-compose.yml create mode 100644 instrumentation/net/http/example/go.mod create mode 100644 instrumentation/net/http/example/go.sum create mode 100644 instrumentation/net/http/example/server/modd.conf create mode 100644 instrumentation/net/http/example/server/server.go create mode 100644 instrumentation/net/http/filters/filters.go create mode 100644 instrumentation/net/http/filters/filters_test.go create mode 100644 instrumentation/net/http/filters/header_go14.go create mode 100644 instrumentation/net/http/filters/header_nongo14.go create mode 100644 instrumentation/net/http/go.mod create mode 100644 instrumentation/net/http/go.sum create mode 100644 instrumentation/net/http/handler.go create mode 100644 instrumentation/net/http/handler_example_test.go create mode 100644 instrumentation/net/http/handler_test.go create mode 100644 instrumentation/net/http/httptrace/api.go create mode 100644 instrumentation/net/http/httptrace/clienttrace.go create mode 100644 instrumentation/net/http/httptrace/clienttrace_test.go create mode 100644 instrumentation/net/http/httptrace/example/Dockerfile create mode 100644 instrumentation/net/http/httptrace/example/README.md create mode 100644 instrumentation/net/http/httptrace/example/client/client.go create mode 100644 instrumentation/net/http/httptrace/example/docker-compose.yml create mode 100644 instrumentation/net/http/httptrace/example/go.mod create mode 100644 instrumentation/net/http/httptrace/example/go.sum create mode 100644 instrumentation/net/http/httptrace/example/server/modd.conf create mode 100644 instrumentation/net/http/httptrace/example/server/server.go create mode 100644 instrumentation/net/http/httptrace/go.mod create mode 100644 instrumentation/net/http/httptrace/go.sum create mode 100644 instrumentation/net/http/httptrace/httptrace.go create mode 100644 instrumentation/net/http/httptrace/httptrace_test.go create mode 100644 instrumentation/net/http/transport.go create mode 100644 instrumentation/net/http/transport_example_test.go create mode 100644 instrumentation/net/http/transport_test.go create mode 100644 instrumentation/net/http/wrap.go create mode 100644 internal/metric/async.go create mode 100644 internal/metric/mock.go create mode 100644 internal/metric/mock_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 79a88438980..43f1818508d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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" diff --git a/.gitignore b/.gitignore index a89a24ec5e4..facddfa5200 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c23f32a37a..6d8aa332303 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/instrumentation/README.md b/instrumentation/README.md index 318260ca27e..e5348a40ab3 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -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 diff --git a/instrumentation/net/http/common.go b/instrumentation/net/http/common.go new file mode 100644 index 00000000000..f4954b1d5b6 --- /dev/null +++ b/instrumentation/net/http/common.go @@ -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 diff --git a/instrumentation/net/http/config.go b/instrumentation/net/http/config.go new file mode 100644 index 00000000000..15fce125d5b --- /dev/null +++ b/instrumentation/net/http/config.go @@ -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 + }) +} diff --git a/instrumentation/net/http/config_test.go b/instrumentation/net/http/config_test.go new file mode 100644 index 00000000000..49297b3a72a --- /dev/null +++ b/instrumentation/net/http/config_test.go @@ -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) + } + }) + } +} diff --git a/instrumentation/net/http/doc.go b/instrumentation/net/http/doc.go new file mode 100644 index 00000000000..0fa9fbbfcb3 --- /dev/null +++ b/instrumentation/net/http/doc.go @@ -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 diff --git a/instrumentation/net/http/example/Dockerfile b/instrumentation/net/http/example/Dockerfile new file mode 100644 index 00000000000..9124d706a22 --- /dev/null +++ b/instrumentation/net/http/example/Dockerfile @@ -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"] diff --git a/instrumentation/net/http/example/README.md b/instrumentation/net/http/example/README.md new file mode 100644 index 00000000000..9da9e2dc813 --- /dev/null +++ b/instrumentation/net/http/example/README.md @@ -0,0 +1,24 @@ +# HTTP Client-Server Example + +An HTTP client connects to an HTTP server. They both generate span information to `stdout`. +These instructions expect you have [docker-compose](https://docs.docker.com/compose/) installed. + +Bring up the `http-server` and `http-client` services to run the example: +```sh +docker-compose up --detach http-server http-client +``` + +The `http-client` service sends just one HTTP request to `http-server` and then exits. View the span generated to `stdout` in the logs: +```sh +docker-compose logs http-client +``` + +View the span generated by `http-server` in the logs: +```sh +docker-compose logs http-server +``` + +Shut down the services when you are finished with the example: +```sh +docker-compose down +``` diff --git a/instrumentation/net/http/example/client/client.go b/instrumentation/net/http/example/client/client.go new file mode 100644 index 00000000000..39ac968dc89 --- /dev/null +++ b/instrumentation/net/http/example/client/client.go @@ -0,0 +1,94 @@ +// 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 main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + + "net/http" + "time" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" + + "go.opentelemetry.io/otel/api/correlation" + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/exporters/stdout" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func initTracer() { + // Create stdout exporter to be able to retrieve + // the collected spans. + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) + if err != nil { + log.Fatal(err) + } + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), + sdktrace.WithSyncer(exporter)) + if err != nil { + log.Fatal(err) + } + global.SetTraceProvider(tp) +} + +func main() { + initTracer() + url := flag.String("server", "http://localhost:7777/hello", "server url") + flag.Parse() + + client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} + + ctx := correlation.NewContext(context.Background(), + kv.String("username", "donuts"), + ) + + var body []byte + + tr := global.Tracer("example/client") + err := tr.WithSpan(ctx, "say hello", + func(ctx context.Context) error { + req, _ := http.NewRequestWithContext(ctx, "GET", *url, nil) + + fmt.Printf("Sending request...\n") + res, err := client.Do(req) + if err != nil { + panic(err) + } + body, err = ioutil.ReadAll(res.Body) + _ = res.Body.Close() + + return err + }, + trace.WithAttributes(standard.PeerServiceKey.String("ExampleService"))) + + if err != nil { + panic(err) + } + + fmt.Printf("Response Received: %s\n\n\n", body) + fmt.Printf("Waiting for few seconds to export spans ...\n\n") + time.Sleep(10 * time.Second) + fmt.Printf("Inspect traces on stdout\n") +} diff --git a/instrumentation/net/http/example/docker-compose.yml b/instrumentation/net/http/example/docker-compose.yml new file mode 100644 index 00000000000..976ec7e83b7 --- /dev/null +++ b/instrumentation/net/http/example/docker-compose.yml @@ -0,0 +1,34 @@ +# 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. +version: "3.7" +services: + http-server: + build: + dockerfile: $PWD/Dockerfile + context: ../../../.. + target: example-http-server + networks: + - example + http-client: + build: + dockerfile: $PWD/Dockerfile + context: ../../../.. + target: example-http-client + command: ["/go/bin/client", "-server", "http://http-server:7777/hello"] + networks: + - example + depends_on: + - http-server +networks: + example: diff --git a/instrumentation/net/http/example/go.mod b/instrumentation/net/http/example/go.mod new file mode 100644 index 00000000000..fcc769da689 --- /dev/null +++ b/instrumentation/net/http/example/go.mod @@ -0,0 +1,15 @@ +module go.opentelemetry.io/contrib/instrumentation/net/http/example + +go 1.14 + +replace ( + go.opentelemetry.io/contrib => ../../../../ + go.opentelemetry.io/contrib/instrumentation/net/http => ../ +) + +require ( + go.opentelemetry.io/contrib/instrumentation/net/http v0.10.0 + go.opentelemetry.io/otel v0.10.0 + go.opentelemetry.io/otel/exporters/stdout v0.10.0 + go.opentelemetry.io/otel/sdk v0.10.0 +) diff --git a/instrumentation/net/http/example/go.sum b/instrumentation/net/http/example/go.sum new file mode 100644 index 00000000000..fde7aa97604 --- /dev/null +++ b/instrumentation/net/http/example/go.sum @@ -0,0 +1,109 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/sketches-go v0.0.1 h1:RtG+76WKgZuz6FIaGsjoPePmadDBkuD/KC6+ZWu78b8= +github.com/DataDog/sketches-go v0.0.1/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.10.0 h1:2y/HYj1dIfG1nPh0Z15X4se8WwYWuTyKHLSgRb/mbQ0= +go.opentelemetry.io/otel v0.10.0/go.mod h1:n3v1JGUBpn5DafiF1UeoDs5fr5XZMG+43kigDtFB8Vk= +go.opentelemetry.io/otel/exporters/stdout v0.10.0 h1:5dhUv/AMKF+9p2igV0pAmS7sWQvX0r+eimf7uiEDWd8= +go.opentelemetry.io/otel/exporters/stdout v0.10.0/go.mod h1:c7hVyiDzqbxgcerYbLreBNI0+MNE8x/hbekVx3lu+gM= +go.opentelemetry.io/otel/sdk v0.10.0 h1:iQWVDfmGB+5TjbrO9yFlezGCWBaJ73vxJTHB+ttdTQk= +go.opentelemetry.io/otel/sdk v0.10.0/go.mod h1:T5752PMr00aUHAVEbaDAYU5tzM2PWOmyy7Lc5OzSrs8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/instrumentation/net/http/example/server/modd.conf b/instrumentation/net/http/example/server/modd.conf new file mode 100644 index 00000000000..22ec99d907d --- /dev/null +++ b/instrumentation/net/http/example/server/modd.conf @@ -0,0 +1,21 @@ +# 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. + +# A basic modd.conf file for Go development. + +# Run go test on ALL modules on startup, and subsequently only on modules +# containing changes. +server.go { + daemon +sigterm: go run server.go +} \ No newline at end of file diff --git a/instrumentation/net/http/example/server/server.go b/instrumentation/net/http/example/server/server.go new file mode 100644 index 00000000000..cbd72894263 --- /dev/null +++ b/instrumentation/net/http/example/server/server.go @@ -0,0 +1,69 @@ +// 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 main + +import ( + "io" + "log" + "net/http" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/exporters/stdout" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func initTracer() { + // Create stdout exporter to be able to retrieve + // the collected spans. + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) + if err != nil { + log.Fatal(err) + } + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), + sdktrace.WithSyncer(exporter), + sdktrace.WithResource(resource.New(standard.ServiceNameKey.String("ExampleService")))) + if err != nil { + log.Fatal(err) + } + global.SetTraceProvider(tp) +} + +func main() { + initTracer() + + helloHandler := func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + span := trace.SpanFromContext(ctx) + span.AddEvent(ctx, "handling this...") + + _, _ = io.WriteString(w, "Hello, world!\n") + } + + otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "Hello") + + http.Handle("/hello", otelHandler) + err := http.ListenAndServe(":7777", nil) + if err != nil { + panic(err) + } +} diff --git a/instrumentation/net/http/filters/filters.go b/instrumentation/net/http/filters/filters.go new file mode 100644 index 00000000000..aedbffca591 --- /dev/null +++ b/instrumentation/net/http/filters/filters.go @@ -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 filters provides a set of filters useful with the +// otelhttp.WithFilter() option to control which inbound requests are traced. +package filters + +import ( + "net/http" + "strings" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" +) + +// Any takes a list of Filters and returns a Filter that +// returns true if any Filter in the list returns true. +func Any(fs ...otelhttp.Filter) otelhttp.Filter { + return func(r *http.Request) bool { + for _, f := range fs { + if f(r) { + return true + } + } + return false + } +} + +// All takes a list of Filters and returns a Filter that +// returns true only if all Filters in the list return true. +func All(fs ...otelhttp.Filter) otelhttp.Filter { + return func(r *http.Request) bool { + for _, f := range fs { + if !f(r) { + return false + } + } + return true + } +} + +// None takes a list of Filters and returns a Filter that returns +// true only if none of the Filters in the list return true. +func None(fs ...otelhttp.Filter) otelhttp.Filter { + return func(r *http.Request) bool { + for _, f := range fs { + if f(r) { + return false + } + } + return true + } +} + +// Not provides a convenience mechanism for inverting a Filter +func Not(f otelhttp.Filter) otelhttp.Filter { + return func(r *http.Request) bool { + return !f(r) + } +} + +// Hostname returns a Filter that returns true if the request's +// hostname matches the provided string. +func Hostname(h string) otelhttp.Filter { + return func(r *http.Request) bool { + return r.URL.Hostname() == h + } +} + +// Path returns a Filter that returns true if the request's +// path matches the provided string. +func Path(p string) otelhttp.Filter { + return func(r *http.Request) bool { + return r.URL.Path == p + } +} + +// PathPrefix returns a Filter that returns true if the request's +// path starts with the provided string. +func PathPrefix(p string) otelhttp.Filter { + return func(r *http.Request) bool { + return strings.HasPrefix(r.URL.Path, p) + } +} + +// Query returns a Filter that returns true if the request +// includes a query parameter k with a value equal to v. +func Query(k, v string) otelhttp.Filter { + return func(r *http.Request) bool { + for _, qv := range r.URL.Query()[k] { + if v == qv { + return true + } + } + return false + } +} + +// QueryContains returns a Filter that returns true if the request +// includes a query parameter k with a value that contains v. +func QueryContains(k, v string) otelhttp.Filter { + return func(r *http.Request) bool { + for _, qv := range r.URL.Query()[k] { + if strings.Contains(qv, v) { + return true + } + } + return false + } +} + +// Method returns a Filter that returns true if the request +// method is equal to the provided value. +func Method(m string) otelhttp.Filter { + return func(r *http.Request) bool { + return m == r.Method + } +} diff --git a/instrumentation/net/http/filters/filters_test.go b/instrumentation/net/http/filters/filters_test.go new file mode 100644 index 00000000000..64e46fa2000 --- /dev/null +++ b/instrumentation/net/http/filters/filters_test.go @@ -0,0 +1,266 @@ +// 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 filters + +import ( + "net/http" + "net/url" + "testing" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" +) + +type scenario struct { + name string + filter otelhttp.Filter + req *http.Request + exp bool +} + +func TestAny(t *testing.T) { + for _, s := range []scenario{ + { + name: "no matching filters", + filter: Any(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/boo", Host: "baz.bar:8080"}}, + exp: false, + }, + { + name: "one matching filter", + filter: Any(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/foo", Host: "baz.bar:8080"}}, + exp: true, + }, + { + name: "all matching filters", + filter: Any(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/foo", Host: "bar.baz:8080"}}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestAll(t *testing.T) { + for _, s := range []scenario{ + { + name: "no matching filters", + filter: All(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/boo", Host: "baz.bar:8080"}}, + exp: false, + }, + { + name: "one matching filter", + filter: All(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/foo", Host: "baz.bar:8080"}}, + exp: false, + }, + { + name: "all matching filters", + filter: All(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/foo", Host: "bar.baz:8080"}}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestNone(t *testing.T) { + for _, s := range []scenario{ + { + name: "no matching filters", + filter: None(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/boo", Host: "baz.bar:8080"}}, + exp: true, + }, + { + name: "one matching filter", + filter: None(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/foo", Host: "baz.bar:8080"}}, + exp: false, + }, + { + name: "all matching filters", + filter: None(Path("/foo"), Hostname("bar.baz")), + req: &http.Request{URL: &url.URL{Path: "/foo", Host: "bar.baz:8080"}}, + exp: false, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestNot(t *testing.T) { + req := &http.Request{URL: &url.URL{Path: "/foo", Host: "bar.baz:8080"}} + filter := Path("/foo") + if filter(req) == Not(filter)(req) { + t.Error("Not filter should invert the result of the supplied filter") + } +} + +func TestPathPrefix(t *testing.T) { + for _, s := range []scenario{ + { + name: "non-matching prefix", + filter: PathPrefix("/foo"), + req: &http.Request{URL: &url.URL{Path: "/boo/far", Host: "baz.bar:8080"}}, + exp: false, + }, + { + name: "matching prefix", + filter: PathPrefix("/foo"), + req: &http.Request{URL: &url.URL{Path: "/foo/bar", Host: "bar.baz:8080"}}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestMethod(t *testing.T) { + for _, s := range []scenario{ + { + name: "non-matching method", + filter: Method(http.MethodGet), + req: &http.Request{Method: http.MethodHead, URL: &url.URL{Path: "/boo/far", Host: "baz.bar:8080"}}, + exp: false, + }, + { + name: "matching method", + filter: Method(http.MethodGet), + req: &http.Request{Method: http.MethodGet, URL: &url.URL{Path: "/boo/far", Host: "baz.bar:8080"}}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestQuery(t *testing.T) { + matching, _ := url.Parse("http://bar.baz:8080/foo/bar?key=value") + nonMatching, _ := url.Parse("http://bar.baz:8080/foo/bar?key=other") + for _, s := range []scenario{ + { + name: "non-matching query parameter", + filter: Query("key", "value"), + req: &http.Request{Method: http.MethodHead, URL: nonMatching}, + exp: false, + }, + { + name: "matching query parameter", + filter: Query("key", "value"), + req: &http.Request{Method: http.MethodGet, URL: matching}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestQueryContains(t *testing.T) { + matching, _ := url.Parse("http://bar.baz:8080/foo/bar?key=value") + nonMatching, _ := url.Parse("http://bar.baz:8080/foo/bar?key=other") + for _, s := range []scenario{ + { + name: "non-matching query parameter", + filter: QueryContains("key", "alu"), + req: &http.Request{Method: http.MethodHead, URL: nonMatching}, + exp: false, + }, + { + name: "matching query parameter", + filter: QueryContains("key", "alu"), + req: &http.Request{Method: http.MethodGet, URL: matching}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestHeader(t *testing.T) { + matching := http.Header{} + matching.Add("key", "value") + nonMatching := http.Header{} + nonMatching.Add("key", "other") + for _, s := range []scenario{ + { + name: "non-matching query parameter", + filter: Header("key", "value"), + req: &http.Request{Method: http.MethodHead, Header: nonMatching}, + exp: false, + }, + { + name: "matching query parameter", + filter: Header("key", "value"), + req: &http.Request{Method: http.MethodGet, Header: matching}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} + +func TestHeaderContains(t *testing.T) { + matching := http.Header{} + matching.Add("key", "value") + nonMatching := http.Header{} + nonMatching.Add("key", "other") + for _, s := range []scenario{ + { + name: "non-matching query parameter", + filter: HeaderContains("key", "alu"), + req: &http.Request{Method: http.MethodHead, Header: nonMatching}, + exp: false, + }, + { + name: "matching query parameter", + filter: HeaderContains("key", "alu"), + req: &http.Request{Method: http.MethodGet, Header: matching}, + exp: true, + }, + } { + res := s.filter(s.req) + if s.exp != res { + t.Errorf("Failed testing %q. Expected %t, got %t", s.name, s.exp, res) + } + } +} diff --git a/instrumentation/net/http/filters/header_go14.go b/instrumentation/net/http/filters/header_go14.go new file mode 100644 index 00000000000..18d1b9bb28d --- /dev/null +++ b/instrumentation/net/http/filters/header_go14.go @@ -0,0 +1,50 @@ +// 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. + +// +build go1.14 + +package filters + +import ( + "net/http" + "strings" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" +) + +// Header returns a Filter that returns true if the request +// includes a header k with a value equal to v. +func Header(k, v string) otelhttp.Filter { + return func(r *http.Request) bool { + for _, hv := range r.Header.Values(k) { + if v == hv { + return true + } + } + return false + } +} + +// HeaderContains returns a Filter that returns true if the request +// includes a header k with a value that contains v. +func HeaderContains(k, v string) otelhttp.Filter { + return func(r *http.Request) bool { + for _, hv := range r.Header.Values(k) { + if strings.Contains(hv, v) { + return true + } + } + return false + } +} diff --git a/instrumentation/net/http/filters/header_nongo14.go b/instrumentation/net/http/filters/header_nongo14.go new file mode 100644 index 00000000000..f9bd6b4f58e --- /dev/null +++ b/instrumentation/net/http/filters/header_nongo14.go @@ -0,0 +1,51 @@ +// 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. + +// +build !go1.14 + +package filters + +import ( + "net/http" + "net/textproto" + "strings" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" +) + +// Header returns a Filter that returns true if the request +// includes a header k with a value equal to v. +func Header(k, v string) otelhttp.Filter { + return func(r *http.Request) bool { + for _, hv := range r.Header[textproto.CanonicalMIMEHeaderKey(k)] { + if v == hv { + return true + } + } + return false + } +} + +// HeaderContains returns a Filter that returns true if the request +// includes a header k with a value that contains v. +func HeaderContains(k, v string) otelhttp.Filter { + return func(r *http.Request) bool { + for _, hv := range r.Header[textproto.CanonicalMIMEHeaderKey(k)] { + if strings.Contains(hv, v) { + return true + } + } + return false + } +} diff --git a/instrumentation/net/http/go.mod b/instrumentation/net/http/go.mod new file mode 100644 index 00000000000..98b0da47e96 --- /dev/null +++ b/instrumentation/net/http/go.mod @@ -0,0 +1,13 @@ +module go.opentelemetry.io/contrib/instrumentation/net/http + +go 1.14 + +replace go.opentelemetry.io/contrib => ../../.. + +require ( + github.com/felixge/httpsnoop v1.0.1 + github.com/stretchr/testify v1.6.1 + go.opentelemetry.io/contrib v0.10.0 + go.opentelemetry.io/otel v0.10.0 + google.golang.org/grpc v1.31.0 +) diff --git a/instrumentation/net/http/go.sum b/instrumentation/net/http/go.sum new file mode 100644 index 00000000000..6c805c980b9 --- /dev/null +++ b/instrumentation/net/http/go.sum @@ -0,0 +1,97 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.10.0 h1:2y/HYj1dIfG1nPh0Z15X4se8WwYWuTyKHLSgRb/mbQ0= +go.opentelemetry.io/otel v0.10.0/go.mod h1:n3v1JGUBpn5DafiF1UeoDs5fr5XZMG+43kigDtFB8Vk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/instrumentation/net/http/handler.go b/instrumentation/net/http/handler.go new file mode 100644 index 00000000000..47c39b4ecaa --- /dev/null +++ b/instrumentation/net/http/handler.go @@ -0,0 +1,221 @@ +// 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" + "net/http" + "time" + + "github.com/felixge/httpsnoop" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/metric" + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" +) + +var _ http.Handler = &Handler{} + +// Handler is http middleware that corresponds to the http.Handler interface and +// is designed to wrap a http.Mux (or equivalent), while individual routes on +// the mux are wrapped with WithRouteTag. A Handler will add various attributes +// to the span using the kv.Keys defined in this package. +type Handler struct { + operation string + handler http.Handler + + tracer trace.Tracer + meter metric.Meter + propagators propagation.Propagators + spanStartOptions []trace.StartOption + readEvent bool + writeEvent bool + filters []Filter + spanNameFormatter func(string, *http.Request) string + counters map[string]metric.Int64Counter + valueRecorders map[string]metric.Int64ValueRecorder +} + +func defaultHandlerFormatter(operation string, _ *http.Request) string { + return operation +} + +// NewHandler wraps the passed handler, functioning like middleware, in a span +// named after the operation and with any provided Options. +func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler { + h := Handler{ + handler: handler, + operation: operation, + } + + const domain = "go.opentelemetry.io/contrib/instrumentation/net/http" + + defaultOpts := []Option{ + WithTracer(global.Tracer(domain)), + WithMeter(global.Meter(domain)), + WithPropagators(global.Propagators()), + WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)), + WithSpanNameFormatter(defaultHandlerFormatter), + } + + c := NewConfig(append(defaultOpts, opts...)...) + h.configure(c) + h.createMeasures() + + return &h +} + +func (h *Handler) configure(c *Config) { + h.tracer = c.Tracer + h.meter = c.Meter + h.propagators = c.Propagators + h.spanStartOptions = c.SpanStartOptions + h.readEvent = c.ReadEvent + h.writeEvent = c.WriteEvent + h.filters = c.Filters + h.spanNameFormatter = c.SpanNameFormatter +} + +func handleErr(err error) { + if err != nil { + global.Handle(err) + } +} + +func (h *Handler) createMeasures() { + h.counters = make(map[string]metric.Int64Counter) + h.valueRecorders = make(map[string]metric.Int64ValueRecorder) + + requestBytesCounter, err := h.meter.NewInt64Counter(RequestContentLength) + handleErr(err) + + responseBytesCounter, err := h.meter.NewInt64Counter(ResponseContentLength) + handleErr(err) + + serverLatencyMeasure, err := h.meter.NewInt64ValueRecorder(ServerLatency) + handleErr(err) + + h.counters[RequestContentLength] = requestBytesCounter + h.counters[ResponseContentLength] = responseBytesCounter + h.valueRecorders[ServerLatency] = serverLatencyMeasure +} + +// ServeHTTP serves HTTP requests (http.Handler) +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + requestStartTime := time.Now() + for _, f := range h.filters { + if !f(r) { + // Simply pass through to the handler if a filter rejects the request + h.handler.ServeHTTP(w, r) + return + } + } + + opts := append([]trace.StartOption{ + trace.WithAttributes(standard.NetAttributesFromHTTPRequest("tcp", r)...), + trace.WithAttributes(standard.EndUserAttributesFromHTTPRequest(r)...), + trace.WithAttributes(standard.HTTPServerAttributesFromHTTPRequest(h.operation, "", r)...), + }, h.spanStartOptions...) // start with the configured options + + ctx := propagation.ExtractHTTP(r.Context(), h.propagators, r.Header) + ctx, span := h.tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...) + defer span.End() + + readRecordFunc := func(int64) {} + if h.readEvent { + readRecordFunc = func(n int64) { + span.AddEvent(ctx, "read", ReadBytesKey.Int64(n)) + } + } + bw := bodyWrapper{ReadCloser: r.Body, record: readRecordFunc} + r.Body = &bw + + writeRecordFunc := func(int64) {} + if h.writeEvent { + writeRecordFunc = func(n int64) { + span.AddEvent(ctx, "write", WroteBytesKey.Int64(n)) + } + } + + rww := &respWriterWrapper{ResponseWriter: w, record: writeRecordFunc, ctx: ctx, props: h.propagators} + + // Wrap w to use our ResponseWriter methods while also exposing + // other interfaces that w may implement (http.CloseNotifier, + // http.Flusher, http.Hijacker, http.Pusher, io.ReaderFrom). + + w = httpsnoop.Wrap(w, httpsnoop.Hooks{ + Header: func(httpsnoop.HeaderFunc) httpsnoop.HeaderFunc { + return rww.Header + }, + Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc { + return rww.Write + }, + WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc { + return rww.WriteHeader + }, + }) + + h.handler.ServeHTTP(w, r.WithContext(ctx)) + + setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err) + + // Add request metrics + + labels := standard.HTTPServerMetricAttributesFromHTTPRequest(h.operation, r) + + h.counters[RequestContentLength].Add(ctx, bw.read, labels...) + h.counters[ResponseContentLength].Add(ctx, rww.written, labels...) + + elapsedTime := time.Since(requestStartTime).Microseconds() + + h.valueRecorders[ServerLatency].Record(ctx, elapsedTime, labels...) +} + +func setAfterServeAttributes(span trace.Span, read, wrote int64, statusCode int, rerr, werr error) { + kv := []kv.KeyValue{} + + // TODO: Consider adding an event after each read and write, possibly as an + // option (defaulting to off), so as to not create needlessly verbose spans. + if read > 0 { + kv = append(kv, ReadBytesKey.Int64(read)) + } + if rerr != nil && rerr != io.EOF { + kv = append(kv, ReadErrorKey.String(rerr.Error())) + } + if wrote > 0 { + kv = append(kv, WroteBytesKey.Int64(wrote)) + } + if statusCode > 0 { + kv = append(kv, standard.HTTPAttributesFromHTTPStatusCode(statusCode)...) + span.SetStatus(standard.SpanStatusFromHTTPStatusCode(statusCode)) + } + if werr != nil && werr != io.EOF { + kv = append(kv, WriteErrorKey.String(werr.Error())) + } + span.SetAttributes(kv...) +} + +// WithRouteTag annotates a span with the provided route name using the +// RouteKey Tag. +func WithRouteTag(route string, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span := trace.SpanFromContext(r.Context()) + span.SetAttributes(standard.HTTPRouteKey.String(route)) + h.ServeHTTP(w, r) + }) +} diff --git a/instrumentation/net/http/handler_example_test.go b/instrumentation/net/http/handler_example_test.go new file mode 100644 index 00000000000..3c2707c2a6e --- /dev/null +++ b/instrumentation/net/http/handler_example_test.go @@ -0,0 +1,98 @@ +// 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_test + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strings" + + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/trace" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" +) + +func ExampleNewHandler() { + /* curl -v -d "a painting" http://localhost:7777/hello/bob/ross + ... + * upload completely sent off: 10 out of 10 bytes + < HTTP/1.1 200 OK + < Traceparent: 00-76ae040ee5753f38edf1c2bd9bd128bd-dd394138cfd7a3dc-01 + < Date: Fri, 04 Oct 2019 02:33:08 GMT + < Content-Length: 45 + < Content-Type: text/plain; charset=utf-8 + < + Hello, bob/ross! + You sent me this: + a painting + */ + + figureOutName := func(ctx context.Context, s string) (string, error) { + pp := strings.SplitN(s, "/", 2) + var err error + switch pp[1] { + case "": + err = fmt.Errorf("expected /hello/:name in %q", s) + default: + trace.SpanFromContext(ctx).SetAttributes(kv.String("name", pp[1])) + } + return pp[1], err + } + + var mux http.ServeMux + mux.Handle("/hello/", + otelhttp.WithRouteTag("/hello/:name", http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var name string + // Wrap another function in its own span + if err := trace.SpanFromContext(ctx).Tracer().WithSpan(ctx, "figureOutName", + func(ctx context.Context) error { + var err error + name, err = figureOutName(ctx, r.URL.Path[1:]) + return err + }); err != nil { + log.Println("error figuring out name: ", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + d, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Println("error reading body: ", err) + w.WriteHeader(http.StatusBadRequest) + 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) + } + }), + ), + ) + + if err := http.ListenAndServe(":7777", + otelhttp.NewHandler(&mux, "server", + otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents), + ), + ); err != nil { + log.Fatal(err) + } +} diff --git a/instrumentation/net/http/handler_test.go b/instrumentation/net/http/handler_test.go new file mode 100644 index 00000000000..ccab0a06a20 --- /dev/null +++ b/instrumentation/net/http/handler_test.go @@ -0,0 +1,164 @@ +// 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 ( + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" + + mockmeter "go.opentelemetry.io/contrib/internal/metric" + mocktrace "go.opentelemetry.io/contrib/internal/trace" +) + +func assertMetricLabels(t *testing.T, expectedLabels []kv.KeyValue, measurementBatches []mockmeter.Batch) { + for _, batch := range measurementBatches { + assert.ElementsMatch(t, expectedLabels, batch.Labels) + } +} + +func TestHandlerBasics(t *testing.T) { + rr := httptest.NewRecorder() + + tracer := mocktrace.Tracer{} + meterimpl, meter := mockmeter.NewMeter() + + operation := "test_handler" + + h := NewHandler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if _, err := io.WriteString(w, "hello world"); err != nil { + t.Fatal(err) + } + }), operation, + WithTracer(&tracer), + WithMeter(meter), + ) + + r, err := http.NewRequest(http.MethodGet, "http://localhost/", strings.NewReader("foo")) + if err != nil { + t.Fatal(err) + } + h.ServeHTTP(rr, r) + + if len(meterimpl.MeasurementBatches) == 0 { + t.Fatalf("got 0 recorded measurements, expected 1 or more") + } + + labelsToVerify := []kv.KeyValue{ + standard.HTTPServerNameKey.String(operation), + standard.HTTPSchemeHTTP, + standard.HTTPHostKey.String(r.Host), + standard.HTTPFlavorKey.String(fmt.Sprintf("1.%d", r.ProtoMinor)), + standard.HTTPRequestContentLengthKey.Int64(3), + } + + assertMetricLabels(t, labelsToVerify, meterimpl.MeasurementBatches) + + 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 non empty trace header") + } + if got, expected := tracer.StartSpanID, uint64(1); 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 TestHandlerNoWrite(t *testing.T) { + rr := httptest.NewRecorder() + + tracer := mocktrace.Tracer{} + + operation := "test_handler" + var span trace.Span + + h := NewHandler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span = trace.SpanFromContext(r.Context()) + }), operation, + WithTracer(&tracer), + ) + + 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(1); got != expected { + t.Fatalf("got %d, expected %d", got, expected) + } + if mockSpan, ok := span.(*mocktrace.Span); ok { + if got, expected := mockSpan.Status, codes.OK; got != expected { + t.Fatalf("got %q, expected %q", got, expected) + } + } else { + t.Fatalf("Expected *moctrace.MockSpan, got %T", span) + } +} + +func TestResponseWriterOptionalInterfaces(t *testing.T) { + rr := httptest.NewRecorder() + + tracer := mocktrace.Tracer{} + + // ResponseRecorder implements the Flusher interface. Make sure the + // wrapped ResponseWriter passed to the handler still implements + // Flusher. + + var isFlusher bool + h := NewHandler( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, isFlusher = w.(http.Flusher) + if _, err := io.WriteString(w, "hello world"); err != nil { + t.Fatal(err) + } + }), "test_handler", + WithTracer(&tracer)) + + r, err := http.NewRequest(http.MethodGet, "http://localhost/", nil) + if err != nil { + t.Fatal(err) + } + h.ServeHTTP(rr, r) + if !isFlusher { + t.Fatal("http.Flusher interface not exposed") + } +} diff --git a/instrumentation/net/http/httptrace/api.go b/instrumentation/net/http/httptrace/api.go new file mode 100644 index 00000000000..9f733aa6672 --- /dev/null +++ b/instrumentation/net/http/httptrace/api.go @@ -0,0 +1,28 @@ +// 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 httptrace + +import ( + "context" + "net/http" + "net/http/httptrace" +) + +// Client +func W3C(ctx context.Context, req *http.Request) (context.Context, *http.Request) { + ctx = httptrace.WithClientTrace(ctx, NewClientTrace(ctx)) + req = req.WithContext(ctx) + return ctx, req +} diff --git a/instrumentation/net/http/httptrace/clienttrace.go b/instrumentation/net/http/httptrace/clienttrace.go new file mode 100644 index 00000000000..454a3c0b1b7 --- /dev/null +++ b/instrumentation/net/http/httptrace/clienttrace.go @@ -0,0 +1,253 @@ +// 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 httptrace + +import ( + "context" + "crypto/tls" + "net/http/httptrace" + "net/textproto" + "strings" + "sync" + + "go.opentelemetry.io/otel/api/standard" + + "google.golang.org/grpc/codes" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/trace" +) + +var ( + HTTPStatus = kv.Key("http.status") + HTTPHeaderMIME = kv.Key("http.mime") + HTTPRemoteAddr = kv.Key("http.remote") + HTTPLocalAddr = kv.Key("http.local") +) + +var ( + hookMap = map[string]string{ + "http.dns": "http.getconn", + "http.connect": "http.getconn", + "http.tls": "http.getconn", + } +) + +func parentHook(hook string) string { + if strings.HasPrefix(hook, "http.connect") { + return hookMap["http.connect"] + } + return hookMap[hook] +} + +type clientTracer struct { + context.Context + + tr trace.Tracer + + activeHooks map[string]context.Context + root trace.Span + mtx sync.Mutex +} + +func NewClientTrace(ctx context.Context) *httptrace.ClientTrace { + ct := &clientTracer{ + Context: ctx, + activeHooks: make(map[string]context.Context), + } + + ct.tr = global.Tracer("go.opentelemetry.io/otel/instrumentation/httptrace") + + return &httptrace.ClientTrace{ + GetConn: ct.getConn, + GotConn: ct.gotConn, + PutIdleConn: ct.putIdleConn, + GotFirstResponseByte: ct.gotFirstResponseByte, + Got100Continue: ct.got100Continue, + Got1xxResponse: ct.got1xxResponse, + DNSStart: ct.dnsStart, + DNSDone: ct.dnsDone, + ConnectStart: ct.connectStart, + ConnectDone: ct.connectDone, + TLSHandshakeStart: ct.tlsHandshakeStart, + TLSHandshakeDone: ct.tlsHandshakeDone, + WroteHeaderField: ct.wroteHeaderField, + WroteHeaders: ct.wroteHeaders, + Wait100Continue: ct.wait100Continue, + WroteRequest: ct.wroteRequest, + } +} + +func (ct *clientTracer) start(hook, spanName string, attrs ...kv.KeyValue) { + ct.mtx.Lock() + defer ct.mtx.Unlock() + + if hookCtx, found := ct.activeHooks[hook]; !found { + var sp trace.Span + ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient)) + if ct.root == nil { + ct.root = sp + } + } else { + // end was called before start finished, add the start attributes and end the span here + span := trace.SpanFromContext(hookCtx) + span.SetAttributes(attrs...) + span.End() + + delete(ct.activeHooks, hook) + } +} + +func (ct *clientTracer) end(hook string, err error, attrs ...kv.KeyValue) { + ct.mtx.Lock() + defer ct.mtx.Unlock() + if ctx, ok := ct.activeHooks[hook]; ok { + span := trace.SpanFromContext(ctx) + if err != nil { + span.SetStatus(codes.Unknown, err.Error()) + } + span.SetAttributes(attrs...) + span.End() + delete(ct.activeHooks, hook) + } else { + // start is not finished before end is called. + // Start a span here with the ending attributes that will be finished when start finishes. + // Yes, it's backwards. v0v + ctx, span := ct.tr.Start(ct.getParentContext(hook), hook, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient)) + if err != nil { + span.SetStatus(codes.Unknown, err.Error()) + } + ct.activeHooks[hook] = ctx + } +} + +func (ct *clientTracer) getParentContext(hook string) context.Context { + ctx, ok := ct.activeHooks[parentHook(hook)] + if !ok { + return ct.Context + } + return ctx +} + +func (ct *clientTracer) span(hook string) trace.Span { + ct.mtx.Lock() + defer ct.mtx.Unlock() + if ctx, ok := ct.activeHooks[hook]; ok { + return trace.SpanFromContext(ctx) + } + return nil +} + +func (ct *clientTracer) getConn(host string) { + ct.start("http.getconn", "http.getconn", standard.HTTPHostKey.String(host)) +} + +func (ct *clientTracer) gotConn(info httptrace.GotConnInfo) { + ct.end("http.getconn", + nil, + HTTPRemoteAddr.String(info.Conn.RemoteAddr().String()), + HTTPLocalAddr.String(info.Conn.LocalAddr().String()), + ) +} + +func (ct *clientTracer) putIdleConn(err error) { + ct.end("http.receive", err) +} + +func (ct *clientTracer) gotFirstResponseByte() { + ct.start("http.receive", "http.receive") +} + +func (ct *clientTracer) dnsStart(info httptrace.DNSStartInfo) { + ct.start("http.dns", "http.dns", standard.HTTPHostKey.String(info.Host)) +} + +func (ct *clientTracer) dnsDone(info httptrace.DNSDoneInfo) { + ct.end("http.dns", info.Err) +} + +func (ct *clientTracer) connectStart(network, addr string) { + ct.start("http.connect."+addr, "http.connect", HTTPRemoteAddr.String(addr)) +} + +func (ct *clientTracer) connectDone(network, addr string, err error) { + ct.end("http.connect."+addr, err) +} + +func (ct *clientTracer) tlsHandshakeStart() { + ct.start("http.tls", "http.tls") +} + +func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) { + ct.end("http.tls", err) +} + +func (ct *clientTracer) wroteHeaderField(k string, v []string) { + if ct.span("http.headers") == nil { + ct.start("http.headers", "http.headers") + } + ct.root.SetAttributes(kv.String("http."+strings.ToLower(k), sliceToString(v))) +} + +func (ct *clientTracer) wroteHeaders() { + if ct.span("http.headers") != nil { + ct.end("http.headers", nil) + } + ct.start("http.send", "http.send") +} + +func (ct *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) { + if info.Err != nil { + ct.root.SetStatus(codes.Unknown, info.Err.Error()) + } + ct.end("http.send", info.Err) +} + +func (ct *clientTracer) got100Continue() { + ct.span("http.receive").AddEvent(ct.Context, "GOT 100 - Continue") +} + +func (ct *clientTracer) wait100Continue() { + ct.span("http.receive").AddEvent(ct.Context, "GOT 100 - Wait") +} + +func (ct *clientTracer) got1xxResponse(code int, header textproto.MIMEHeader) error { + ct.span("http.receive").AddEvent(ct.Context, "GOT 1xx", + HTTPStatus.Int(code), + HTTPHeaderMIME.String(sm2s(header)), + ) + return nil +} + +func sliceToString(value []string) string { + if len(value) == 0 { + return "undefined" + } + return strings.Join(value, ",") +} + +func sm2s(value map[string][]string) string { + var buf strings.Builder + for k, v := range value { + if buf.Len() != 0 { + buf.WriteString(",") + } + buf.WriteString(k) + buf.WriteString("=") + buf.WriteString(sliceToString(v)) + } + return buf.String() +} diff --git a/instrumentation/net/http/httptrace/clienttrace_test.go b/instrumentation/net/http/httptrace/clienttrace_test.go new file mode 100644 index 00000000000..db213a6f040 --- /dev/null +++ b/instrumentation/net/http/httptrace/clienttrace_test.go @@ -0,0 +1,238 @@ +// 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 httptrace_test + +import ( + "context" + "net/http" + "net/http/httptest" + nhtrace "net/http/httptrace" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/trace/testtrace" + "go.opentelemetry.io/otel/instrumentation/httptrace" +) + +type SpanRecorder map[string]*testtrace.Span + +func (sr *SpanRecorder) OnStart(span *testtrace.Span) {} +func (sr *SpanRecorder) OnEnd(span *testtrace.Span) { (*sr)[span.Name()] = span } + +func TestHTTPRequestWithClientTrace(t *testing.T) { + sr := SpanRecorder{} + tp := testtrace.NewProvider(testtrace.WithSpanRecorder(&sr)) + global.SetTraceProvider(tp) + tr := tp.Tracer("httptrace/client") + + // Mock http server + ts := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + }), + ) + defer ts.Close() + address := ts.Listener.Addr() + + client := ts.Client() + err := tr.WithSpan(context.Background(), "test", + func(ctx context.Context) error { + req, _ := http.NewRequest("GET", ts.URL, nil) + _, req = httptrace.W3C(ctx, req) + + res, err := client.Do(req) + if err != nil { + t.Fatalf("Request failed: %s", err.Error()) + } + _ = res.Body.Close() + + return nil + }) + if err != nil { + panic("unexpected error in http request: " + err.Error()) + } + + testLen := []struct { + name string + attributes map[kv.Key]kv.Value + parent string + }{ + { + name: "http.connect", + attributes: map[kv.Key]kv.Value{ + kv.Key("http.remote"): kv.StringValue(address.String()), + }, + parent: "http.getconn", + }, + { + name: "http.getconn", + attributes: map[kv.Key]kv.Value{ + kv.Key("http.remote"): kv.StringValue(address.String()), + kv.Key("http.host"): kv.StringValue(address.String()), + }, + parent: "test", + }, + { + name: "http.receive", + parent: "test", + }, + { + name: "http.headers", + parent: "test", + }, + { + name: "http.send", + parent: "test", + }, + { + name: "test", + }, + } + for _, tl := range testLen { + if !assert.Contains(t, sr, tl.name) { + continue + } + span := sr[tl.name] + if tl.parent != "" { + if assert.Contains(t, sr, tl.parent) { + assert.Equal(t, span.ParentSpanID(), sr[tl.parent].SpanContext().SpanID) + } + } + if len(tl.attributes) > 0 { + attrs := span.Attributes() + if tl.name == "http.getconn" { + // http.local attribute uses a non-deterministic port. + local := kv.Key("http.local") + assert.Contains(t, attrs, local) + delete(attrs, local) + } + assert.Equal(t, tl.attributes, attrs) + } + } +} + +type MultiSpanRecorder map[string][]*testtrace.Span + +func (sr *MultiSpanRecorder) Reset() { (*sr) = MultiSpanRecorder{} } +func (sr *MultiSpanRecorder) OnStart(span *testtrace.Span) {} +func (sr *MultiSpanRecorder) OnEnd(span *testtrace.Span) { + (*sr)[span.Name()] = append((*sr)[span.Name()], span) +} + +func TestConcurrentConnectionStart(t *testing.T) { + sr := MultiSpanRecorder{} + global.SetTraceProvider( + testtrace.NewProvider(testtrace.WithSpanRecorder(&sr)), + ) + ct := httptrace.NewClientTrace(context.Background()) + tts := []struct { + name string + run func() + }{ + { + name: "Open1Close1Open2Close2", + run: func() { + ct.ConnectStart("tcp", "127.0.0.1:3000") + ct.ConnectDone("tcp", "127.0.0.1:3000", nil) + ct.ConnectStart("tcp", "[::1]:3000") + ct.ConnectDone("tcp", "[::1]:3000", nil) + }, + }, + { + name: "Open2Close2Open1Close1", + run: func() { + ct.ConnectStart("tcp", "[::1]:3000") + ct.ConnectDone("tcp", "[::1]:3000", nil) + ct.ConnectStart("tcp", "127.0.0.1:3000") + ct.ConnectDone("tcp", "127.0.0.1:3000", nil) + }, + }, + { + name: "Open1Open2Close1Close2", + run: func() { + ct.ConnectStart("tcp", "127.0.0.1:3000") + ct.ConnectStart("tcp", "[::1]:3000") + ct.ConnectDone("tcp", "127.0.0.1:3000", nil) + ct.ConnectDone("tcp", "[::1]:3000", nil) + }, + }, + { + name: "Open1Open2Close2Close1", + run: func() { + ct.ConnectStart("tcp", "127.0.0.1:3000") + ct.ConnectStart("tcp", "[::1]:3000") + ct.ConnectDone("tcp", "[::1]:3000", nil) + ct.ConnectDone("tcp", "127.0.0.1:3000", nil) + }, + }, + { + name: "Open2Open1Close1Close2", + run: func() { + ct.ConnectStart("tcp", "[::1]:3000") + ct.ConnectStart("tcp", "127.0.0.1:3000") + ct.ConnectDone("tcp", "127.0.0.1:3000", nil) + ct.ConnectDone("tcp", "[::1]:3000", nil) + }, + }, + { + name: "Open2Open1Close2Close1", + run: func() { + ct.ConnectStart("tcp", "[::1]:3000") + ct.ConnectStart("tcp", "127.0.0.1:3000") + ct.ConnectDone("tcp", "[::1]:3000", nil) + ct.ConnectDone("tcp", "127.0.0.1:3000", nil) + }, + }, + } + + expectedRemotes := []kv.KeyValue{ + kv.String("http.remote", "127.0.0.1:3000"), + kv.String("http.remote", "[::1]:3000"), + } + for _, tt := range tts { + t.Run(tt.name, func(t *testing.T) { + sr.Reset() + tt.run() + spans := sr["http.connect"] + require.Len(t, spans, 2) + + var gotRemotes []kv.KeyValue + for _, span := range spans { + for k, v := range span.Attributes() { + gotRemotes = append(gotRemotes, kv.Any(string(k), v.AsInterface())) + } + } + assert.ElementsMatch(t, expectedRemotes, gotRemotes) + }) + } +} + +func TestEndBeforeStartCreatesSpan(t *testing.T) { + sr := MultiSpanRecorder{} + global.SetTraceProvider( + testtrace.NewProvider(testtrace.WithSpanRecorder(&sr)), + ) + + ct := httptrace.NewClientTrace(context.Background()) + ct.DNSDone(nhtrace.DNSDoneInfo{}) + ct.DNSStart(nhtrace.DNSStartInfo{Host: "example.com"}) + + name := "http.dns" + require.Contains(t, sr, name) + spans := sr[name] + require.Len(t, spans, 1) +} diff --git a/instrumentation/net/http/httptrace/example/Dockerfile b/instrumentation/net/http/httptrace/example/Dockerfile new file mode 100644 index 00000000000..2d80618ec8a --- /dev/null +++ b/instrumentation/net/http/httptrace/example/Dockerfile @@ -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/httptrace/example + +FROM base AS example-httptrace-server +RUN go install ./server/server.go +CMD ["/go/bin/server"] + +FROM base AS example-httptrace-client +RUN go install ./client/client.go +CMD ["/go/bin/client"] diff --git a/instrumentation/net/http/httptrace/example/README.md b/instrumentation/net/http/httptrace/example/README.md new file mode 100644 index 00000000000..9da9e2dc813 --- /dev/null +++ b/instrumentation/net/http/httptrace/example/README.md @@ -0,0 +1,24 @@ +# HTTP Client-Server Example + +An HTTP client connects to an HTTP server. They both generate span information to `stdout`. +These instructions expect you have [docker-compose](https://docs.docker.com/compose/) installed. + +Bring up the `http-server` and `http-client` services to run the example: +```sh +docker-compose up --detach http-server http-client +``` + +The `http-client` service sends just one HTTP request to `http-server` and then exits. View the span generated to `stdout` in the logs: +```sh +docker-compose logs http-client +``` + +View the span generated by `http-server` in the logs: +```sh +docker-compose logs http-server +``` + +Shut down the services when you are finished with the example: +```sh +docker-compose down +``` diff --git a/instrumentation/net/http/httptrace/example/client/client.go b/instrumentation/net/http/httptrace/example/client/client.go new file mode 100644 index 00000000000..38ae8a1ffff --- /dev/null +++ b/instrumentation/net/http/httptrace/example/client/client.go @@ -0,0 +1,98 @@ +// 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 main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + + "net/http" + "net/http/httptrace" + "time" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" + otelhttptrace "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace" + + "go.opentelemetry.io/otel/api/correlation" + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/exporters/stdout" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func initTracer() { + // Create stdout exporter to be able to retrieve + // the collected spans. + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) + if err != nil { + log.Fatal(err) + } + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), + sdktrace.WithSyncer(exporter)) + if err != nil { + log.Fatal(err) + } + global.SetTraceProvider(tp) +} + +func main() { + initTracer() + url := flag.String("server", "http://localhost:7777/hello", "server url") + flag.Parse() + + client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} + + ctx := correlation.NewContext(context.Background(), + kv.String("username", "donuts"), + ) + + ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx)) + + var body []byte + + tr := global.Tracer("example/client") + err := tr.WithSpan(ctx, "say hello", + func(ctx context.Context) error { + req, _ := http.NewRequestWithContext(ctx, "GET", *url, nil) + + fmt.Printf("Sending request...\n") + res, err := client.Do(req) + if err != nil { + panic(err) + } + body, err = ioutil.ReadAll(res.Body) + _ = res.Body.Close() + + return err + }, + trace.WithAttributes(standard.PeerServiceKey.String("ExampleService"))) + + if err != nil { + panic(err) + } + + fmt.Printf("Response Received: %s\n\n\n", body) + fmt.Printf("Waiting for few seconds to export spans ...\n\n") + time.Sleep(10 * time.Second) + fmt.Printf("Inspect traces on stdout\n") +} diff --git a/instrumentation/net/http/httptrace/example/docker-compose.yml b/instrumentation/net/http/httptrace/example/docker-compose.yml new file mode 100644 index 00000000000..8c5d1dd155c --- /dev/null +++ b/instrumentation/net/http/httptrace/example/docker-compose.yml @@ -0,0 +1,34 @@ +# 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. +version: "3.7" +services: + http-server: + build: + dockerfile: $PWD/Dockerfile + context: ../../../../.. + target: example-httptrace-server + networks: + - example + http-client: + build: + dockerfile: $PWD/Dockerfile + context: ../../../../.. + target: example-httptrace-client + command: ["/go/bin/client", "-server", "http://http-server:7777/hello"] + networks: + - example + depends_on: + - http-server +networks: + example: diff --git a/instrumentation/net/http/httptrace/example/go.mod b/instrumentation/net/http/httptrace/example/go.mod new file mode 100644 index 00000000000..199664f0151 --- /dev/null +++ b/instrumentation/net/http/httptrace/example/go.mod @@ -0,0 +1,17 @@ +module go.opentelemetry.io/contrib/instrumentation/net/http/example + +go 1.14 + +replace ( + go.opentelemetry.io/contrib => ../../../../../ + go.opentelemetry.io/contrib/instrumentation/net/http => ../../ + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace => ../ +) + +require ( + go.opentelemetry.io/contrib/instrumentation/net/http v0.10.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace v0.10.0 + go.opentelemetry.io/otel v0.10.0 + go.opentelemetry.io/otel/exporters/stdout v0.10.0 + go.opentelemetry.io/otel/sdk v0.10.0 +) diff --git a/instrumentation/net/http/httptrace/example/go.sum b/instrumentation/net/http/httptrace/example/go.sum new file mode 100644 index 00000000000..fde7aa97604 --- /dev/null +++ b/instrumentation/net/http/httptrace/example/go.sum @@ -0,0 +1,109 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/sketches-go v0.0.1 h1:RtG+76WKgZuz6FIaGsjoPePmadDBkuD/KC6+ZWu78b8= +github.com/DataDog/sketches-go v0.0.1/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.10.0 h1:2y/HYj1dIfG1nPh0Z15X4se8WwYWuTyKHLSgRb/mbQ0= +go.opentelemetry.io/otel v0.10.0/go.mod h1:n3v1JGUBpn5DafiF1UeoDs5fr5XZMG+43kigDtFB8Vk= +go.opentelemetry.io/otel/exporters/stdout v0.10.0 h1:5dhUv/AMKF+9p2igV0pAmS7sWQvX0r+eimf7uiEDWd8= +go.opentelemetry.io/otel/exporters/stdout v0.10.0/go.mod h1:c7hVyiDzqbxgcerYbLreBNI0+MNE8x/hbekVx3lu+gM= +go.opentelemetry.io/otel/sdk v0.10.0 h1:iQWVDfmGB+5TjbrO9yFlezGCWBaJ73vxJTHB+ttdTQk= +go.opentelemetry.io/otel/sdk v0.10.0/go.mod h1:T5752PMr00aUHAVEbaDAYU5tzM2PWOmyy7Lc5OzSrs8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/instrumentation/net/http/httptrace/example/server/modd.conf b/instrumentation/net/http/httptrace/example/server/modd.conf new file mode 100644 index 00000000000..22ec99d907d --- /dev/null +++ b/instrumentation/net/http/httptrace/example/server/modd.conf @@ -0,0 +1,21 @@ +# 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. + +# A basic modd.conf file for Go development. + +# Run go test on ALL modules on startup, and subsequently only on modules +# containing changes. +server.go { + daemon +sigterm: go run server.go +} \ No newline at end of file diff --git a/instrumentation/net/http/httptrace/example/server/server.go b/instrumentation/net/http/httptrace/example/server/server.go new file mode 100644 index 00000000000..cbd72894263 --- /dev/null +++ b/instrumentation/net/http/httptrace/example/server/server.go @@ -0,0 +1,69 @@ +// 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 main + +import ( + "io" + "log" + "net/http" + + otelhttp "go.opentelemetry.io/contrib/instrumentation/net/http" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" + "go.opentelemetry.io/otel/exporters/stdout" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" +) + +func initTracer() { + // Create stdout exporter to be able to retrieve + // the collected spans. + exporter, err := stdout.NewExporter(stdout.WithPrettyPrint()) + if err != nil { + log.Fatal(err) + } + + // For the demonstration, use sdktrace.AlwaysSample sampler to sample all traces. + // In a production application, use sdktrace.ProbabilitySampler with a desired probability. + tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}), + sdktrace.WithSyncer(exporter), + sdktrace.WithResource(resource.New(standard.ServiceNameKey.String("ExampleService")))) + if err != nil { + log.Fatal(err) + } + global.SetTraceProvider(tp) +} + +func main() { + initTracer() + + helloHandler := func(w http.ResponseWriter, req *http.Request) { + ctx := req.Context() + span := trace.SpanFromContext(ctx) + span.AddEvent(ctx, "handling this...") + + _, _ = io.WriteString(w, "Hello, world!\n") + } + + otelHandler := otelhttp.NewHandler(http.HandlerFunc(helloHandler), "Hello") + + http.Handle("/hello", otelHandler) + err := http.ListenAndServe(":7777", nil) + if err != nil { + panic(err) + } +} diff --git a/instrumentation/net/http/httptrace/go.mod b/instrumentation/net/http/httptrace/go.mod new file mode 100644 index 00000000000..da42de3f29c --- /dev/null +++ b/instrumentation/net/http/httptrace/go.mod @@ -0,0 +1,12 @@ +module go.opentelemetry.io/contrib/instrumentation/net/http/httptrace + +go 1.14 + +replace go.opentelemetry.io/contrib => ../../../.. + +require ( + github.com/google/go-cmp v0.5.1 + github.com/stretchr/testify v1.6.1 + go.opentelemetry.io/otel v0.10.0 + google.golang.org/grpc v1.31.0 +) diff --git a/instrumentation/net/http/httptrace/go.sum b/instrumentation/net/http/httptrace/go.sum new file mode 100644 index 00000000000..153a3498199 --- /dev/null +++ b/instrumentation/net/http/httptrace/go.sum @@ -0,0 +1,94 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.10.0 h1:2y/HYj1dIfG1nPh0Z15X4se8WwYWuTyKHLSgRb/mbQ0= +go.opentelemetry.io/otel v0.10.0/go.mod h1:n3v1JGUBpn5DafiF1UeoDs5fr5XZMG+43kigDtFB8Vk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/instrumentation/net/http/httptrace/httptrace.go b/instrumentation/net/http/httptrace/httptrace.go new file mode 100644 index 00000000000..4f382217dc6 --- /dev/null +++ b/instrumentation/net/http/httptrace/httptrace.go @@ -0,0 +1,74 @@ +// 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 httptrace + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/api/correlation" + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" +) + +// Option is a function that allows configuration of the httptrace Extract() +// and Inject() functions +type Option func(*config) + +type config struct { + propagators propagation.Propagators +} + +func newConfig(opts []Option) *config { + c := &config{propagators: global.Propagators()} + for _, o := range opts { + o(c) + } + return c +} + +// WithPropagators sets the propagators to use for Extraction and Injection +func WithPropagators(props propagation.Propagators) Option { + return func(c *config) { + c.propagators = props + } +} + +// Returns the Attributes, Context Entries, and SpanContext that were encoded by Inject. +func Extract(ctx context.Context, req *http.Request, opts ...Option) ([]kv.KeyValue, []kv.KeyValue, trace.SpanContext) { + c := newConfig(opts) + ctx = propagation.ExtractHTTP(ctx, c.propagators, req.Header) + + attrs := append( + standard.HTTPServerAttributesFromHTTPRequest("", "", req), + standard.NetAttributesFromHTTPRequest("tcp", req)..., + ) + + var correlationCtxKVs []kv.KeyValue + correlation.MapFromContext(ctx).Foreach(func(kv kv.KeyValue) bool { + correlationCtxKVs = append(correlationCtxKVs, kv) + return true + }) + + return attrs, correlationCtxKVs, trace.RemoteSpanContextFromContext(ctx) +} + +func Inject(ctx context.Context, req *http.Request, opts ...Option) { + c := newConfig(opts) + propagation.InjectHTTP(ctx, c.propagators, req.Header) +} diff --git a/instrumentation/net/http/httptrace/httptrace_test.go b/instrumentation/net/http/httptrace/httptrace_test.go new file mode 100644 index 00000000000..06aa9782d2f --- /dev/null +++ b/instrumentation/net/http/httptrace/httptrace_test.go @@ -0,0 +1,164 @@ +// 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 httptrace_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/otel/api/correlation" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace/testtrace" + "go.opentelemetry.io/otel/instrumentation/httptrace" +) + +func TestRoundtrip(t *testing.T) { + tr := testtrace.NewProvider().Tracer("httptrace/client") + + var expectedAttrs map[kv.Key]string + expectedCorrs := map[kv.Key]string{kv.Key("foo"): "bar"} + + // Mock http server + ts := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attrs, corrs, span := httptrace.Extract(r.Context(), r) + + actualAttrs := make(map[kv.Key]string) + for _, attr := range attrs { + if attr.Key == standard.NetPeerPortKey { + // Peer port will be non-deterministic + continue + } + actualAttrs[attr.Key] = attr.Value.Emit() + } + + if diff := cmp.Diff(actualAttrs, expectedAttrs); diff != "" { + t.Fatalf("[TestRoundtrip] Attributes are different: %v", diff) + } + + actualCorrs := make(map[kv.Key]string) + for _, corr := range corrs { + actualCorrs[corr.Key] = corr.Value.Emit() + } + + if diff := cmp.Diff(actualCorrs, expectedCorrs); diff != "" { + t.Fatalf("[TestRoundtrip] Correlations are different: %v", diff) + } + + if !span.IsValid() { + t.Fatalf("[TestRoundtrip] Invalid span extracted: %v", span) + } + + _, err := w.Write([]byte("OK")) + if err != nil { + t.Fatal(err) + } + }), + ) + defer ts.Close() + + address := ts.Listener.Addr() + hp := strings.Split(address.String(), ":") + expectedAttrs = map[kv.Key]string{ + standard.HTTPFlavorKey: "1.1", + standard.HTTPHostKey: address.String(), + standard.HTTPMethodKey: "GET", + standard.HTTPSchemeKey: "http", + standard.HTTPTargetKey: "/", + standard.HTTPUserAgentKey: "Go-http-client/1.1", + standard.HTTPRequestContentLengthKey: "3", + standard.NetHostIPKey: hp[0], + standard.NetHostPortKey: hp[1], + standard.NetPeerIPKey: "127.0.0.1", + standard.NetTransportKey: "IP.TCP", + } + + client := ts.Client() + err := tr.WithSpan(context.Background(), "test", + func(ctx context.Context) error { + ctx = correlation.ContextWithMap(ctx, correlation.NewMap(correlation.MapUpdate{SingleKV: kv.Key("foo").String("bar")})) + req, _ := http.NewRequest("GET", ts.URL, strings.NewReader("foo")) + httptrace.Inject(ctx, req) + + res, err := client.Do(req) + if err != nil { + t.Fatalf("Request failed: %s", err.Error()) + } + _ = res.Body.Close() + + return nil + }) + if err != nil { + panic("unexpected error in http request: " + err.Error()) + } +} + +func TestSpecifyPropagators(t *testing.T) { + tr := testtrace.NewProvider().Tracer("httptrace/client") + + expectedCorrs := map[kv.Key]string{kv.Key("foo"): "bar"} + + // Mock http server + ts := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, corrs, span := httptrace.Extract(r.Context(), r, httptrace.WithPropagators(propagation.New(propagation.WithExtractors(correlation.DefaultHTTPPropagator())))) + + actualCorrs := make(map[kv.Key]string) + for _, corr := range corrs { + actualCorrs[corr.Key] = corr.Value.Emit() + } + + if diff := cmp.Diff(actualCorrs, expectedCorrs); diff != "" { + t.Fatalf("[TestRoundtrip] Correlations are different: %v", diff) + } + + if span.IsValid() { + t.Fatalf("[TestRoundtrip] valid span extracted, expected none: %v", span) + } + + _, err := w.Write([]byte("OK")) + if err != nil { + t.Fatal(err) + } + }), + ) + defer ts.Close() + + client := ts.Client() + err := tr.WithSpan(context.Background(), "test", + func(ctx context.Context) error { + ctx = correlation.ContextWithMap(ctx, correlation.NewMap(correlation.MapUpdate{SingleKV: kv.Key("foo").String("bar")})) + req, _ := http.NewRequest("GET", ts.URL, nil) + httptrace.Inject(ctx, req, httptrace.WithPropagators(propagation.New(propagation.WithInjectors(correlation.DefaultHTTPPropagator())))) + + res, err := client.Do(req) + if err != nil { + t.Fatalf("Request failed: %s", err.Error()) + } + _ = res.Body.Close() + + return nil + }) + if err != nil { + panic("unexpected error in http request: " + err.Error()) + } +} diff --git a/instrumentation/net/http/transport.go b/instrumentation/net/http/transport.go new file mode 100644 index 00000000000..3bfe177af15 --- /dev/null +++ b/instrumentation/net/http/transport.go @@ -0,0 +1,134 @@ +// 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" + "io" + "net/http" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/standard" + "go.opentelemetry.io/otel/api/trace" + + "google.golang.org/grpc/codes" +) + +// Transport implements the http.RoundTripper interface and wraps +// outbound HTTP(S) requests with a span. +type Transport struct { + rt http.RoundTripper + + tracer trace.Tracer + propagators propagation.Propagators + spanStartOptions []trace.StartOption + filters []Filter + spanNameFormatter func(string, *http.Request) string +} + +var _ http.RoundTripper = &Transport{} + +// NewTransport wraps the provided http.RoundTripper with one that +// starts a span and injects the span context into the outbound request headers. +func NewTransport(base http.RoundTripper, opts ...Option) *Transport { + t := Transport{ + rt: base, + } + + defaultOpts := []Option{ + WithTracer(global.Tracer("go.opentelemetry.io/contrib/instrumentation/net/http")), + WithPropagators(global.Propagators()), + WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)), + WithSpanNameFormatter(defaultTransportFormatter), + } + + c := NewConfig(append(defaultOpts, opts...)...) + t.configure(c) + + return &t +} + +func (t *Transport) configure(c *Config) { + t.tracer = c.Tracer + t.propagators = c.Propagators + t.spanStartOptions = c.SpanStartOptions + t.filters = c.Filters + t.spanNameFormatter = c.SpanNameFormatter +} + +func defaultTransportFormatter(_ string, r *http.Request) string { + return r.Method +} + +// RoundTrip creates a Span and propagates its context via the provided request's headers +// before handing the request to the configured base RoundTripper. The created span will +// end when the response body is closed or when a read from the body returns io.EOF. +func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) { + for _, f := range t.filters { + if !f(r) { + // Simply pass through to the base RoundTripper if a filter rejects the request + return t.rt.RoundTrip(r) + } + } + + opts := append([]trace.StartOption{}, t.spanStartOptions...) // start with the configured options + + ctx, span := t.tracer.Start(r.Context(), t.spanNameFormatter("", r), opts...) + + r = r.WithContext(ctx) + span.SetAttributes(standard.HTTPClientAttributesFromHTTPRequest(r)...) + propagation.InjectHTTP(ctx, t.propagators, r.Header) + + res, err := t.rt.RoundTrip(r) + if err != nil { + span.RecordError(ctx, err, trace.WithErrorStatus(codes.Internal)) + span.End() + return res, err + } + + span.SetAttributes(standard.HTTPAttributesFromHTTPStatusCode(res.StatusCode)...) + span.SetStatus(standard.SpanStatusFromHTTPStatusCode(res.StatusCode)) + res.Body = &wrappedBody{ctx: ctx, span: span, body: res.Body} + + return res, err +} + +type wrappedBody struct { + ctx context.Context + span trace.Span + body io.ReadCloser +} + +var _ io.ReadCloser = &wrappedBody{} + +func (wb *wrappedBody) Read(b []byte) (int, error) { + n, err := wb.body.Read(b) + + switch err { + case nil: + // nothing to do here but fall through to the return + case io.EOF: + wb.span.End() + default: + wb.span.RecordError(wb.ctx, err, trace.WithErrorStatus(codes.Internal)) + } + return n, err +} + +func (wb *wrappedBody) Close() error { + wb.span.End() + return wb.body.Close() +} diff --git a/instrumentation/net/http/transport_example_test.go b/instrumentation/net/http/transport_example_test.go new file mode 100644 index 00000000000..e4a1ddb2014 --- /dev/null +++ b/instrumentation/net/http/transport_example_test.go @@ -0,0 +1,27 @@ +// 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" +) + +func ExampleNewTransport() { + // Create an http.Client that uses the (ot)http.Transport + // wrapped around the http.DefaultTransport + _ = http.Client{ + Transport: NewTransport(http.DefaultTransport), + } +} diff --git a/instrumentation/net/http/transport_test.go b/instrumentation/net/http/transport_test.go new file mode 100644 index 00000000000..5e2b8c18d18 --- /dev/null +++ b/instrumentation/net/http/transport_test.go @@ -0,0 +1,76 @@ +// 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 ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/propagation" + "go.opentelemetry.io/otel/api/trace" + + mocktrace "go.opentelemetry.io/contrib/internal/trace" +) + +func TestTransportBasics(t *testing.T) { + tracer := mocktrace.Tracer{} + content := []byte("Hello, world!") + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := propagation.ExtractHTTP(r.Context(), global.Propagators(), r.Header) + span := trace.RemoteSpanContextFromContext(ctx) + tgtID, err := trace.SpanIDFromHex(fmt.Sprintf("%016x", tracer.StartSpanID)) + if err != nil { + t.Fatalf("Error converting id to SpanID: %s", err.Error()) + } + if span.SpanID != tgtID { + t.Fatalf("testing remote SpanID: got %s, expected %s", span.SpanID, tgtID) + } + if _, err := w.Write(content); err != nil { + t.Fatal(err) + } + })) + defer ts.Close() + + r, err := http.NewRequest(http.MethodGet, ts.URL, nil) + if err != nil { + t.Fatal(err) + } + + tr := NewTransport( + http.DefaultTransport, + WithTracer(&tracer), + ) + + c := http.Client{Transport: tr} + res, err := c.Do(r) + if err != nil { + t.Fatal(err) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(body, content) { + t.Fatalf("unexpected content: got %s, expected %s", body, content) + } +} diff --git a/instrumentation/net/http/wrap.go b/instrumentation/net/http/wrap.go new file mode 100644 index 00000000000..f9ddb248c16 --- /dev/null +++ b/instrumentation/net/http/wrap.go @@ -0,0 +1,96 @@ +// 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" + "io" + "net/http" + + "go.opentelemetry.io/otel/api/propagation" +) + +var _ io.ReadCloser = &bodyWrapper{} + +// bodyWrapper wraps a http.Request.Body (an io.ReadCloser) to track the number +// of bytes read and the last error +type bodyWrapper struct { + io.ReadCloser + record func(n int64) // must not be nil + + read int64 + err error +} + +func (w *bodyWrapper) Read(b []byte) (int, error) { + n, err := w.ReadCloser.Read(b) + n1 := int64(n) + w.read += n1 + w.err = err + w.record(n1) + return n, err +} + +func (w *bodyWrapper) Close() error { + return w.ReadCloser.Close() +} + +var _ http.ResponseWriter = &respWriterWrapper{} + +// respWriterWrapper wraps a http.ResponseWriter in order to track the number of +// bytes written, the last error, and to catch the returned statusCode +// TODO: The wrapped http.ResponseWriter doesn't implement any of the optional +// types (http.Hijacker, http.Pusher, http.CloseNotifier, http.Flusher, etc) +// that may be useful when using it in real life situations. +type respWriterWrapper struct { + http.ResponseWriter + record func(n int64) // must not be nil + + // used to inject the header + ctx context.Context + + props propagation.Propagators + + written int64 + statusCode int + err error + wroteHeader bool +} + +func (w *respWriterWrapper) Header() http.Header { + return w.ResponseWriter.Header() +} + +func (w *respWriterWrapper) Write(p []byte) (int, error) { + if !w.wroteHeader { + w.WriteHeader(http.StatusOK) + } + n, err := w.ResponseWriter.Write(p) + n1 := int64(n) + w.record(n1) + w.written += n1 + w.err = err + return n, err +} + +func (w *respWriterWrapper) WriteHeader(statusCode int) { + if w.wroteHeader { + return + } + w.wroteHeader = true + w.statusCode = statusCode + propagation.InjectHTTP(w.ctx, w.props, w.Header()) + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/internal/metric/async.go b/internal/metric/async.go new file mode 100644 index 00000000000..e57e8fd00a2 --- /dev/null +++ b/internal/metric/async.go @@ -0,0 +1,148 @@ +// 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 metric + +import ( + "context" + "errors" + "fmt" + "sync" + + "go.opentelemetry.io/otel/api/global" + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/metric" +) + +var ErrInvalidAsyncRunner = errors.New("unknown async runner type") + +// AsyncCollector is an interface used between the MeterImpl and the +// AsyncInstrumentState helper below. This interface is implemented by +// the SDK to provide support for running observer callbacks. +type AsyncCollector interface { + // CollectAsync passes a batch of observations to the MeterImpl. + CollectAsync([]kv.KeyValue, ...metric.Observation) +} + +// AsyncInstrumentState manages an ordered set of asynchronous +// instruments and the distinct runners, taking into account batch +// observer callbacks. +type AsyncInstrumentState struct { + lock sync.Mutex + + // errorOnce will use the global.Handler to report an error + // once in case of an invalid runner attempting to run. + errorOnce sync.Once + + // runnerMap keeps the set of runners that will run each + // collection interval. Singletons are entered with a real + // instrument each, batch observers are entered with a nil + // instrument, ensuring that when a singleton callback is used + // repeatedly, it is excuted repeatedly in the interval, while + // when a batch callback is used repeatedly, it only executes + // once per interval. + runnerMap map[asyncRunnerPair]struct{} + + // runners maintains the set of runners in the order they were + // registered. + runners []asyncRunnerPair + + // instruments maintains the set of instruments in the order + // they were registered. + instruments []metric.AsyncImpl +} + +// asyncRunnerPair is a map entry for Observer callback runners. +type asyncRunnerPair struct { + // runner is used as a map key here. The API ensures + // that all callbacks are pointers for this reason. + runner metric.AsyncRunner + + // inst refers to a non-nil instrument when `runner` is a + // AsyncSingleRunner. + inst metric.AsyncImpl +} + +// NewAsyncInstrumentState returns a new *AsyncInstrumentState, for +// use by MeterImpl to manage running the set of observer callbacks in +// the correct order. +func NewAsyncInstrumentState() *AsyncInstrumentState { + return &AsyncInstrumentState{ + runnerMap: map[asyncRunnerPair]struct{}{}, + } +} + +// Instruments returns the asynchronous instruments managed by this +// object, the set that should be checkpointed after observers are +// run. +func (a *AsyncInstrumentState) Instruments() []metric.AsyncImpl { + a.lock.Lock() + defer a.lock.Unlock() + return a.instruments +} + +// Register adds a new asynchronous instrument to by managed by this +// object. This should be called during NewAsyncInstrument() and +// assumes that errors (e.g., duplicate registration) have already +// been checked. +func (a *AsyncInstrumentState) Register(inst metric.AsyncImpl, runner metric.AsyncRunner) { + a.lock.Lock() + defer a.lock.Unlock() + + a.instruments = append(a.instruments, inst) + + // asyncRunnerPair reflects this callback in the asyncRunners + // list. If this is a batch runner, the instrument is nil. + // If this is a single-Observer runner, the instrument is + // included. This ensures that batch callbacks are called + // once and single callbacks are called once per instrument. + rp := asyncRunnerPair{ + runner: runner, + } + if _, ok := runner.(metric.AsyncSingleRunner); ok { + rp.inst = inst + } + + if _, ok := a.runnerMap[rp]; !ok { + a.runnerMap[rp] = struct{}{} + a.runners = append(a.runners, rp) + } +} + +// Run executes the complete set of observer callbacks. +func (a *AsyncInstrumentState) Run(ctx context.Context, collector AsyncCollector) { + a.lock.Lock() + runners := a.runners + a.lock.Unlock() + + for _, rp := range runners { + // The runner must be a single or batch runner, no + // other implementations are possible because the + // interface has un-exported methods. + + if singleRunner, ok := rp.runner.(metric.AsyncSingleRunner); ok { + singleRunner.Run(ctx, rp.inst, collector.CollectAsync) + continue + } + + if multiRunner, ok := rp.runner.(metric.AsyncBatchRunner); ok { + multiRunner.Run(ctx, collector.CollectAsync) + continue + } + + a.errorOnce.Do(func() { + global.Handle(fmt.Errorf("%w: type %T (reported once)", ErrInvalidAsyncRunner, rp)) + }) + } +} diff --git a/internal/metric/mock.go b/internal/metric/mock.go new file mode 100644 index 00000000000..c6fb2fd0300 --- /dev/null +++ b/internal/metric/mock.go @@ -0,0 +1,191 @@ +// 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 metric + +import ( + "context" + "sync" + + "go.opentelemetry.io/otel/api/kv" + "go.opentelemetry.io/otel/api/metric" + apimetric "go.opentelemetry.io/otel/api/metric" + "go.opentelemetry.io/otel/api/metric/registry" +) + +type ( + Handle struct { + Instrument *Sync + Labels []kv.KeyValue + } + + Batch struct { + // Measurement needs to be aligned for 64-bit atomic operations. + Measurements []Measurement + Ctx context.Context + Labels []kv.KeyValue + LibraryName string + } + + MeterImpl struct { + lock sync.Mutex + + MeasurementBatches []Batch + + asyncInstruments *AsyncInstrumentState + } + + Measurement struct { + // Number needs to be aligned for 64-bit atomic operations. + Number apimetric.Number + Instrument apimetric.InstrumentImpl + } + + Instrument struct { + meter *MeterImpl + descriptor apimetric.Descriptor + } + + Async struct { + Instrument + + runner apimetric.AsyncRunner + } + + Sync struct { + Instrument + } +) + +var ( + _ apimetric.SyncImpl = &Sync{} + _ apimetric.BoundSyncImpl = &Handle{} + _ apimetric.MeterImpl = &MeterImpl{} + _ apimetric.AsyncImpl = &Async{} +) + +func (i Instrument) Descriptor() apimetric.Descriptor { + return i.descriptor +} + +func (a *Async) Implementation() interface{} { + return a +} + +func (s *Sync) Implementation() interface{} { + return s +} + +func (s *Sync) Bind(labels []kv.KeyValue) apimetric.BoundSyncImpl { + return &Handle{ + Instrument: s, + Labels: labels, + } +} + +func (s *Sync) RecordOne(ctx context.Context, number apimetric.Number, labels []kv.KeyValue) { + s.meter.doRecordSingle(ctx, labels, s, number) +} + +func (h *Handle) RecordOne(ctx context.Context, number apimetric.Number) { + h.Instrument.meter.doRecordSingle(ctx, h.Labels, h.Instrument, number) +} + +func (h *Handle) Unbind() { +} + +func (m *MeterImpl) doRecordSingle(ctx context.Context, labels []kv.KeyValue, instrument apimetric.InstrumentImpl, number apimetric.Number) { + m.collect(ctx, labels, []Measurement{{ + Instrument: instrument, + Number: number, + }}) +} + +func NewProvider() (*MeterImpl, apimetric.Provider) { + impl := &MeterImpl{ + asyncInstruments: NewAsyncInstrumentState(), + } + return impl, registry.NewProvider(impl) +} + +func NewMeter() (*MeterImpl, apimetric.Meter) { + impl, p := NewProvider() + return impl, p.Meter("mock") +} + +func (m *MeterImpl) NewSyncInstrument(descriptor metric.Descriptor) (apimetric.SyncImpl, error) { + m.lock.Lock() + defer m.lock.Unlock() + + return &Sync{ + Instrument{ + descriptor: descriptor, + meter: m, + }, + }, nil +} + +func (m *MeterImpl) NewAsyncInstrument(descriptor metric.Descriptor, runner metric.AsyncRunner) (apimetric.AsyncImpl, error) { + m.lock.Lock() + defer m.lock.Unlock() + + a := &Async{ + Instrument: Instrument{ + descriptor: descriptor, + meter: m, + }, + runner: runner, + } + m.asyncInstruments.Register(a, runner) + return a, nil +} + +func (m *MeterImpl) RecordBatch(ctx context.Context, labels []kv.KeyValue, measurements ...apimetric.Measurement) { + mm := make([]Measurement, len(measurements)) + for i := 0; i < len(measurements); i++ { + m := measurements[i] + mm[i] = Measurement{ + Instrument: m.SyncImpl().Implementation().(*Sync), + Number: m.Number(), + } + } + m.collect(ctx, labels, mm) +} + +func (m *MeterImpl) CollectAsync(labels []kv.KeyValue, obs ...metric.Observation) { + mm := make([]Measurement, len(obs)) + for i := 0; i < len(obs); i++ { + o := obs[i] + mm[i] = Measurement{ + Instrument: o.AsyncImpl(), + Number: o.Number(), + } + } + m.collect(context.Background(), labels, mm) +} + +func (m *MeterImpl) collect(ctx context.Context, labels []kv.KeyValue, measurements []Measurement) { + m.lock.Lock() + defer m.lock.Unlock() + + m.MeasurementBatches = append(m.MeasurementBatches, Batch{ + Ctx: ctx, + Labels: labels, + Measurements: measurements, + }) +} + +func (m *MeterImpl) RunAsyncInstruments() { + m.asyncInstruments.Run(context.Background(), m) +} diff --git a/internal/metric/mock_test.go b/internal/metric/mock_test.go new file mode 100644 index 00000000000..561fb2e596b --- /dev/null +++ b/internal/metric/mock_test.go @@ -0,0 +1,76 @@ +// 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 metric + +import ( + "fmt" + "io" + "os" + "testing" + "unsafe" +) + +// FieldOffset is a preprocessor representation of a struct field alignment. +type FieldOffset struct { + // Name of the field. + Name string + + // Offset of the field in bytes. + // + // To compute this at compile time use unsafe.Offsetof. + Offset uintptr +} + +// Aligned8Byte returns if all fields are aligned modulo 8-bytes. +// +// Error messaging is printed to out for any fileds determined misaligned. +func Aligned8Byte(fields []FieldOffset, out io.Writer) bool { + misaligned := make([]FieldOffset, 0) + for _, f := range fields { + if f.Offset%8 != 0 { + misaligned = append(misaligned, f) + } + } + + if len(misaligned) == 0 { + return true + } + + fmt.Fprintln(out, "struct fields not aligned for 64-bit atomic operations:") + for _, f := range misaligned { + fmt.Fprintf(out, " %s: %d-byte offset\n", f.Name, f.Offset) + } + + return false +} + +// Ensure struct alignment prior to running tests. +func TestMain(m *testing.M) { + fields := []FieldOffset{ + { + Name: "Batch.Measurments", + Offset: unsafe.Offsetof(Batch{}.Measurements), + }, + { + Name: "Measurement.Number", + Offset: unsafe.Offsetof(Measurement{}.Number), + }, + } + if !Aligned8Byte(fields, os.Stderr) { + os.Exit(1) + } + + os.Exit(m.Run()) +}