mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2024-12-10 09:50:58 +02:00
Create basic othttp.Transport for simple client instrumentation (#678)
* Create basic othttp.Transport for simple client instrumentation * change props to propagators for clarity * Add othttp.Transport usage example Co-authored-by: Joshua MacDonald <jmacd@users.noreply.github.com>
This commit is contained in:
parent
e4ec924b25
commit
973b38607b
54
plugin/othttp/common.go
Normal file
54
plugin/othttp/common.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// 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 othttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
"go.opentelemetry.io/otel/api/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attribute keys that can be added to a span.
|
||||||
|
const (
|
||||||
|
HostKey = core.Key("http.host") // the HTTP host (http.Request.Host)
|
||||||
|
MethodKey = core.Key("http.method") // the HTTP method (http.Request.Method)
|
||||||
|
PathKey = core.Key("http.path") // the HTTP path (http.Request.URL.Path)
|
||||||
|
URLKey = core.Key("http.url") // the HTTP URL (http.Request.URL.String())
|
||||||
|
UserAgentKey = core.Key("http.user_agent") // the HTTP user agent (http.Request.UserAgent())
|
||||||
|
RouteKey = core.Key("http.route") // the HTTP route (ex: /users/:id)
|
||||||
|
RemoteAddrKey = core.Key("http.remote_addr") // the network address of the client that sent the HTTP request (http.Request.RemoteAddr)
|
||||||
|
StatusCodeKey = core.Key("http.status_code") // if set, the HTTP status
|
||||||
|
ReadBytesKey = core.Key("http.read_bytes") // if anything was read from the request body, the total number of bytes read
|
||||||
|
ReadErrorKey = core.Key("http.read_error") // If an error occurred while reading a request, the string of the error (io.EOF is not recorded)
|
||||||
|
WroteBytesKey = core.Key("http.wrote_bytes") // if anything was written to the response writer, the total number of bytes written
|
||||||
|
WriteErrorKey = core.Key("http.write_error") // if an error occurred while writing a reply, the string of the error (io.EOF is not recorded)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Setup basic span attributes before so that they
|
||||||
|
// are available to be mutated if needed.
|
||||||
|
func setBasicAttributes(span trace.Span, r *http.Request) {
|
||||||
|
span.SetAttributes(
|
||||||
|
HostKey.String(r.Host),
|
||||||
|
MethodKey.String(r.Method),
|
||||||
|
PathKey.String(r.URL.Path),
|
||||||
|
URLKey.String(r.URL.String()),
|
||||||
|
UserAgentKey.String(r.UserAgent()),
|
||||||
|
)
|
||||||
|
}
|
140
plugin/othttp/config.go
Normal file
140
plugin/othttp/config.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// 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 othttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/propagation"
|
||||||
|
"go.opentelemetry.io/otel/api/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the configuration options available for the othttp.Handler
|
||||||
|
// and othttp.Transport types.
|
||||||
|
type Config struct {
|
||||||
|
Tracer trace.Tracer
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
})
|
||||||
|
}
|
131
plugin/othttp/config_test.go
Normal file
131
plugin/othttp/config_test.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
// 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 othttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mocktrace "go.opentelemetry.io/otel/internal/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasicFilter(t *testing.T) {
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
var id uint64
|
||||||
|
tracer := mocktrace.MockTracer{StartSpanID: &id}
|
||||||
|
|
||||||
|
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 := id, 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 id uint64
|
||||||
|
var spanName string
|
||||||
|
tracer := mocktrace.MockTracer{
|
||||||
|
StartSpanID: &id,
|
||||||
|
OnSpanStarted: func(span *mocktrace.MockSpan) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -26,26 +26,6 @@ import (
|
|||||||
|
|
||||||
var _ http.Handler = &Handler{}
|
var _ http.Handler = &Handler{}
|
||||||
|
|
||||||
// Attribute keys that the Handler can add to a span.
|
|
||||||
const (
|
|
||||||
HostKey = core.Key("http.host") // the HTTP host (http.Request.Host)
|
|
||||||
MethodKey = core.Key("http.method") // the HTTP method (http.Request.Method)
|
|
||||||
PathKey = core.Key("http.path") // the HTTP path (http.Request.URL.Path)
|
|
||||||
URLKey = core.Key("http.url") // the HTTP URL (http.Request.URL.String())
|
|
||||||
UserAgentKey = core.Key("http.user_agent") // the HTTP user agent (http.Request.UserAgent())
|
|
||||||
RouteKey = core.Key("http.route") // the HTTP route (ex: /users/:id)
|
|
||||||
RemoteAddrKey = core.Key("http.remote_addr") // the network address of the client that sent the HTTP request (http.Request.RemoteAddr)
|
|
||||||
StatusCodeKey = core.Key("http.status_code") // if set, the HTTP status
|
|
||||||
ReadBytesKey = core.Key("http.read_bytes") // if anything was read from the request body, the total number of bytes read
|
|
||||||
ReadErrorKey = core.Key("http.read_error") // If an error occurred while reading a request, the string of the error (io.EOF is not recorded)
|
|
||||||
WroteBytesKey = core.Key("http.wrote_bytes") // if anything was written to the response writer, the total number of bytes written
|
|
||||||
WriteErrorKey = core.Key("http.write_error") // if an error occurred while writing a reply, the string of the error (io.EOF is not recorded)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
// Handler is http middleware that corresponds to the http.Handler interface and
|
// 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
|
// 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
|
// the mux are wrapped with WithRouteTag. A Handler will add various attributes
|
||||||
@ -55,7 +35,7 @@ type Handler struct {
|
|||||||
handler http.Handler
|
handler http.Handler
|
||||||
|
|
||||||
tracer trace.Tracer
|
tracer trace.Tracer
|
||||||
props propagation.Propagators
|
propagators propagation.Propagators
|
||||||
spanStartOptions []trace.StartOption
|
spanStartOptions []trace.StartOption
|
||||||
readEvent bool
|
readEvent bool
|
||||||
writeEvent bool
|
writeEvent bool
|
||||||
@ -63,118 +43,41 @@ type Handler struct {
|
|||||||
spanNameFormatter func(string, *http.Request) string
|
spanNameFormatter func(string, *http.Request) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option function used for setting *optional* Handler properties
|
func defaultHandlerFormatter(operation string, _ *http.Request) string {
|
||||||
type Option func(*Handler)
|
|
||||||
|
|
||||||
// WithTracer configures the Handler with a specific tracer. If this option
|
|
||||||
// isn't specified then the global tracer is used.
|
|
||||||
func WithTracer(tracer trace.Tracer) Option {
|
|
||||||
return func(h *Handler) {
|
|
||||||
h.tracer = tracer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 func(h *Handler) {
|
|
||||||
h.spanStartOptions = append(h.spanStartOptions, trace.WithNewRoot())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPropagators configures the Handler with 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 func(h *Handler) {
|
|
||||||
h.props = ps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSpanOptions configures the Handler with an additional set of
|
|
||||||
// trace.StartOptions, which are applied to each new span.
|
|
||||||
func WithSpanOptions(opts ...trace.StartOption) Option {
|
|
||||||
return func(h *Handler) {
|
|
||||||
h.spanStartOptions = append(h.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 func(h *Handler) {
|
|
||||||
h.filters = append(h.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 func(h *Handler) {
|
|
||||||
for _, e := range events {
|
|
||||||
switch e {
|
|
||||||
case ReadEvents:
|
|
||||||
h.readEvent = true
|
|
||||||
case WriteEvents:
|
|
||||||
h.writeEvent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSpanNameFormatter takes a function that will be called on every
|
|
||||||
// incoming request and the returned string will become the Span Name
|
|
||||||
func WithSpanNameFormatter(f func(operation string, r *http.Request) string) Option {
|
|
||||||
return func(h *Handler) {
|
|
||||||
h.spanNameFormatter = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultFormatter(operation string, _ *http.Request) string {
|
|
||||||
return operation
|
return operation
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler wraps the passed handler, functioning like middleware, in a span
|
// NewHandler wraps the passed handler, functioning like middleware, in a span
|
||||||
// named after the operation and with any provided HandlerOptions.
|
// named after the operation and with any provided Options.
|
||||||
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
|
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
|
||||||
h := Handler{
|
h := Handler{
|
||||||
handler: handler,
|
handler: handler,
|
||||||
operation: operation,
|
operation: operation,
|
||||||
spanNameFormatter: defaultFormatter,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultOpts := []Option{
|
defaultOpts := []Option{
|
||||||
WithTracer(global.Tracer("go.opentelemetry.io/plugin/othttp")),
|
WithTracer(global.Tracer("go.opentelemetry.io/plugin/othttp")),
|
||||||
WithPropagators(global.Propagators()),
|
WithPropagators(global.Propagators()),
|
||||||
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
|
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
|
||||||
|
WithSpanNameFormatter(defaultHandlerFormatter),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, opt := range append(defaultOpts, opts...) {
|
c := NewConfig(append(defaultOpts, opts...)...)
|
||||||
opt(&h)
|
h.configure(c)
|
||||||
}
|
|
||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) configure(c *Config) {
|
||||||
|
h.tracer = c.Tracer
|
||||||
|
h.propagators = c.Propagators
|
||||||
|
h.spanStartOptions = c.SpanStartOptions
|
||||||
|
h.readEvent = c.ReadEvent
|
||||||
|
h.writeEvent = c.WriteEvent
|
||||||
|
h.filters = c.Filters
|
||||||
|
h.spanNameFormatter = c.SpanNameFormatter
|
||||||
|
}
|
||||||
|
|
||||||
// ServeHTTP serves HTTP requests (http.Handler)
|
// ServeHTTP serves HTTP requests (http.Handler)
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
for _, f := range h.filters {
|
for _, f := range h.filters {
|
||||||
@ -187,7 +90,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
opts := append([]trace.StartOption{}, h.spanStartOptions...) // start with the configured options
|
opts := append([]trace.StartOption{}, h.spanStartOptions...) // start with the configured options
|
||||||
|
|
||||||
ctx := propagation.ExtractHTTP(r.Context(), h.props, r.Header)
|
ctx := propagation.ExtractHTTP(r.Context(), h.propagators, r.Header)
|
||||||
ctx, span := h.tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...)
|
ctx, span := h.tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...)
|
||||||
defer span.End()
|
defer span.End()
|
||||||
|
|
||||||
@ -207,18 +110,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rww := &respWriterWrapper{ResponseWriter: w, record: writeRecordFunc, ctx: ctx, props: h.props}
|
rww := &respWriterWrapper{ResponseWriter: w, record: writeRecordFunc, ctx: ctx, props: h.propagators}
|
||||||
|
|
||||||
// Setup basic span attributes before calling handler.ServeHTTP so that they
|
setBasicAttributes(span, r)
|
||||||
// are available to be mutated by the handler if needed.
|
span.SetAttributes(RemoteAddrKey.String(r.RemoteAddr))
|
||||||
span.SetAttributes(
|
|
||||||
HostKey.String(r.Host),
|
|
||||||
MethodKey.String(r.Method),
|
|
||||||
PathKey.String(r.URL.Path),
|
|
||||||
URLKey.String(r.URL.String()),
|
|
||||||
UserAgentKey.String(r.UserAgent()),
|
|
||||||
RemoteAddrKey.String(r.RemoteAddr),
|
|
||||||
)
|
|
||||||
|
|
||||||
h.handler.ServeHTTP(rww, r.WithContext(ctx))
|
h.handler.ServeHTTP(rww, r.WithContext(ctx))
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
mocktrace "go.opentelemetry.io/otel/internal/trace"
|
mocktrace "go.opentelemetry.io/otel/internal/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasics(t *testing.T) {
|
func TestHandlerBasics(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
var id uint64
|
var id uint64
|
||||||
@ -59,104 +59,3 @@ func TestBasics(t *testing.T) {
|
|||||||
t.Fatalf("got %q, expected %q", got, expected)
|
t.Fatalf("got %q, expected %q", got, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasicFilter(t *testing.T) {
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
|
|
||||||
var id uint64
|
|
||||||
tracer := mocktrace.MockTracer{StartSpanID: &id}
|
|
||||||
|
|
||||||
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 := id, 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 formatter",
|
|
||||||
formatter: defaultFormatter,
|
|
||||||
operation: "test_operation",
|
|
||||||
expected: "test_operation",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 id uint64
|
|
||||||
var spanName string
|
|
||||||
tracer := mocktrace.MockTracer{
|
|
||||||
StartSpanID: &id,
|
|
||||||
OnSpanStarted: func(span *mocktrace.MockSpan) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
132
plugin/othttp/transport.go
Normal file
132
plugin/othttp/transport.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
// 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 othttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/global"
|
||||||
|
"go.opentelemetry.io/otel/api/propagation"
|
||||||
|
"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/plugin/othttp")),
|
||||||
|
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)
|
||||||
|
setBasicAttributes(span, 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(StatusCodeKey.Int(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()
|
||||||
|
}
|
59
plugin/othttp/transport_example_test.go
Normal file
59
plugin/othttp/transport_example_test.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// 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 othttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/global"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNewTransport() {
|
||||||
|
// Start with a working trace provider
|
||||||
|
tp, err := sdktrace.NewProvider(sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
global.SetTraceProvider(tp)
|
||||||
|
|
||||||
|
// Create an http.Client that uses the othttp.Transport
|
||||||
|
// wrapped around the http.DefaultTransport
|
||||||
|
client := http.Client{
|
||||||
|
Transport: NewTransport(http.DefaultTransport),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a request with our tracing client
|
||||||
|
response, err := client.Get("https://postman-echo.com/get")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the whole body and close it. The span created by the
|
||||||
|
// othttp.Transport does not end until a read from the response
|
||||||
|
// body returns io.EOF or the response body is closed.
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s", body)
|
||||||
|
// body should look like this, with a different "traceparent" value:
|
||||||
|
// {"args":{},"headers":{"x-forwarded-proto":"https","host":"postman-echo.com","accept-encoding":"gzip","traceparent":"00-fb1d6775b94db561d9b51adbb3640de5-919c41073ec08f50-01","user-agent":"Go-http-client/1.1","x-forwarded-port":"443"},"url":"https://postman-echo.com/get"}
|
||||||
|
}
|
77
plugin/othttp/transport_test.go
Normal file
77
plugin/othttp/transport_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 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 othttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
"go.opentelemetry.io/otel/api/global"
|
||||||
|
"go.opentelemetry.io/otel/api/propagation"
|
||||||
|
"go.opentelemetry.io/otel/api/trace"
|
||||||
|
mocktrace "go.opentelemetry.io/otel/internal/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTransportBasics(t *testing.T) {
|
||||||
|
var id uint64
|
||||||
|
tracer := mocktrace.MockTracer{StartSpanID: &id}
|
||||||
|
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 := core.SpanIDFromHex(fmt.Sprintf("%016x", id))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user