Skip to content

Commit

Permalink
Fix up docs and other othttp cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
freeformz committed Oct 17, 2019
1 parent 11bdacf commit c4aed58
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 310 deletions.
18 changes: 18 additions & 0 deletions plugin/othttp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2019, 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 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 othttp
228 changes: 228 additions & 0 deletions plugin/othttp/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// Copyright 2019, 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"
"net/http"

"go.opentelemetry.io/api/core"
"go.opentelemetry.io/api/propagation"
"go.opentelemetry.io/api/trace"
prop "go.opentelemetry.io/propagation"
)

var _ http.Handler = &Handler{}

// Attribute keys that the Handler can add to a span.
var (
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)
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)
)

// 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 core.Keys defined in this package.
type Handler struct {
operation string
handler http.Handler

tracer trace.Tracer
prop propagation.TextFormatPropagator
spanOptions []trace.SpanOption
public bool
readEvent bool
writeEvent bool
}

// Option function used for setting *optional* Handler properties
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.public = true
}
}

// WithPropagator configures the Handler with a specific propagator. If this
// option isn't specificed then
// go.opentelemetry.io/propagation.HTTPTraceContextPropagator is used.
func WithPropagator(p propagation.TextFormatPropagator) Option {
return func(h *Handler) {
h.prop = p
}
}

// WithSpanOptions configures the Handler with an additional set of
// trace.SpanOptions, which are applied to each new span.
func WithSpanOptions(opts ...trace.SpanOption) Option {
return func(h *Handler) {
h.spanOptions = opts
}
}

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
}
}
}
}

// NewHandler wraps the passed handler, functioning like middleware, in a span
// named after the operation and with any provided HandlerOptions.
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
h := Handler{handler: handler, operation: operation}
defaultOpts := []Option{
WithTracer(trace.GlobalTracer()),
WithPropagator(prop.HTTPTraceContextPropagator{}),
}

for _, opt := range append(defaultOpts, opts...) {
opt(&h)
}
return &h
}

// ServeHTTP serves HTTP requests (http.Handler)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
opts := append([]trace.SpanOption{}, h.spanOptions...) // start with the configured options

sc := h.prop.Extract(r.Context(), r.Header)
if sc.IsValid() { // not a valid span context, so no link / parent relationship to establish
var opt trace.SpanOption
if h.public {
// TODO: If the endpoint is a public endpoint, it should start a new trace
// and incoming remote sctx should be added as a link
// (WithLinks(links...), this option doesn't exist yet). Replace ChildOf
// below with something like: opt = trace.WithLinks(sc)
opt = trace.ChildOf(sc)
} else { // not a private endpoint, so assume child relationship
opt = trace.ChildOf(sc)
}
opts = append(opts, opt)
}

ctx, span := h.tracer.Start(r.Context(), h.operation, 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, injector: h.prop}

// Setup basic span attributes before calling handler.ServeHTTP so that they
// are available to be mutated by the handler if needed.
span.SetAttributes(
HostKey.String(r.Host),
MethodKey.String(r.Method),
PathKey.String(r.URL.Path),
URLKey.String(r.URL.String()),
UserAgentKey.String(r.UserAgent()),
)

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

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

func setAfterServeAttributes(span trace.Span, read, wrote, statusCode int64, rerr, werr error) {
kv := make([]core.KeyValue, 0, 5)
// 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, StatusCodeKey.Int64(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.CurrentSpan(r.Context())
//TODO: Why doesn't tag.Upsert work?
span.SetAttribute(RouteKey.String(route))
h.ServeHTTP(w, r.WithContext(trace.SetCurrentSpan(r.Context(), span)))
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,7 @@ func ExampleNewHandler() {
case "":
err = fmt.Errorf("expected /hello/:name in %q", s)
default:
span := trace.CurrentSpan(ctx)
span.SetAttribute(
core.KeyValue{Key: "name",
Value: core.Value{Type: core.STRING, String: pp[1]},
},
)
trace.CurrentSpan(ctx).SetAttribute(core.Key("name").String(pp[1]))
}
return pp[1], err
}
Expand Down Expand Up @@ -113,7 +108,7 @@ func ExampleNewHandler() {

if err := http.ListenAndServe(":7777",
othttp.NewHandler(&mux, "server",
othttp.WithMessageEvents(othttp.EventRead, othttp.EventWrite),
othttp.WithMessageEvents(othttp.ReadEvents, othttp.WriteEvents),
),
); err != nil {
log.Fatal(err)
Expand Down
File renamed without changes.
Loading

0 comments on commit c4aed58

Please sign in to comment.