Skip to content

Commit

Permalink
Add redaction to request / response
Browse files Browse the repository at this point in the history
Adding redaction option to request and response so consumer of the library can hide sensitive values

References: open-telemetry#3895
  • Loading branch information
fpozzobon committed Jun 1, 2023
1 parent 0332bf5 commit 71b8100
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 36 deletions.
41 changes: 27 additions & 14 deletions instrumentation/net/http/otelhttp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"net/http/httptrace"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
Expand All @@ -32,18 +33,19 @@ const (
// config represents the configuration options available for the http.Handler
// and http.Transport types.
type config struct {
ServerName string
Tracer trace.Tracer
Meter metric.Meter
Propagators propagation.TextMapPropagator
SpanStartOptions []trace.SpanStartOption
PublicEndpoint bool
PublicEndpointFn func(*http.Request) bool
ReadEvent bool
WriteEvent bool
Filters []Filter
SpanNameFormatter func(string, *http.Request) string
ClientTrace func(context.Context) *httptrace.ClientTrace
ServerName string
Tracer trace.Tracer
Meter metric.Meter
Propagators propagation.TextMapPropagator
SpanStartOptions []trace.SpanStartOption
PublicEndpoint bool
PublicEndpointFn func(*http.Request) bool
ReadEvent bool
WriteEvent bool
Filters []Filter
SpanNameFormatter func(string, *http.Request) string
ClientTrace func(context.Context) *httptrace.ClientTrace
RedactedAttributes map[attribute.Key]struct{}

TracerProvider trace.TracerProvider
MeterProvider metric.MeterProvider
Expand All @@ -63,8 +65,9 @@ func (o optionFunc) apply(c *config) {
// newConfig creates a new config struct and applies opts to it.
func newConfig(opts ...Option) *config {
c := &config{
Propagators: otel.GetTextMapPropagator(),
MeterProvider: otel.GetMeterProvider(),
Propagators: otel.GetTextMapPropagator(),
MeterProvider: otel.GetMeterProvider(),
RedactedAttributes: map[attribute.Key]struct{}{},
}
for _, opt := range opts {
opt.apply(c)
Expand Down Expand Up @@ -153,6 +156,16 @@ func WithFilter(f Filter) Option {
})
}

// WithRedactedAttributes will be replaced by fixed '****' values for the request / response
// names provided. By default, no request / response are redacted.
func WithRedactedAttributes(attributes ...attribute.Key) Option {
return optionFunc(func(ct *config) {
for _, att := range attributes {
ct.RedactedAttributes[att] = struct{}{}
}
})
}

type event int

// Different types of events that can be recorded, see WithMessageEvents.
Expand Down
47 changes: 33 additions & 14 deletions instrumentation/net/http/otelhttp/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,19 @@ type middleware struct {
operation string
server string

tracer trace.Tracer
meter metric.Meter
propagators propagation.TextMapPropagator
spanStartOptions []trace.SpanStartOption
readEvent bool
writeEvent bool
filters []Filter
spanNameFormatter func(string, *http.Request) string
counters map[string]metric.Int64Counter
valueRecorders map[string]metric.Float64Histogram
publicEndpoint bool
publicEndpointFn func(*http.Request) bool
tracer trace.Tracer
meter metric.Meter
propagators propagation.TextMapPropagator
spanStartOptions []trace.SpanStartOption
readEvent bool
writeEvent bool
filters []Filter
redactedAttributes map[attribute.Key]struct{}
spanNameFormatter func(string, *http.Request) string
counters map[string]metric.Int64Counter
valueRecorders map[string]metric.Float64Histogram
publicEndpoint bool
publicEndpointFn func(*http.Request) bool
}

func defaultHandlerFormatter(operation string, _ *http.Request) string {
Expand Down Expand Up @@ -91,6 +92,7 @@ func (h *middleware) configure(c *config) {
h.readEvent = c.ReadEvent
h.writeEvent = c.WriteEvent
h.filters = c.Filters
h.redactedAttributes = c.RedactedAttributes
h.spanNameFormatter = c.SpanNameFormatter
h.publicEndpoint = c.PublicEndpoint
h.publicEndpointFn = c.PublicEndpointFn
Expand Down Expand Up @@ -135,7 +137,7 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http

ctx := h.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
opts := []trace.SpanStartOption{
trace.WithAttributes(semconvutil.HTTPServerRequest(h.server, r)...),
trace.WithAttributes(h.redactAttributes(semconvutil.HTTPServerRequest(h.server, r))...),
}
if h.server != "" {
hostAttr := semconv.NetHostName(h.server)
Expand Down Expand Up @@ -223,7 +225,7 @@ func (h *middleware) serveHTTP(w http.ResponseWriter, r *http.Request, next http
if rww.statusCode > 0 {
attributes = append(attributes, semconv.HTTPStatusCode(rww.statusCode))
}
o := metric.WithAttributes(attributes...)
o := metric.WithAttributes(h.redactAttributes(attributes)...)
h.counters[RequestContentLength].Add(ctx, bw.read, o)
h.counters[ResponseContentLength].Add(ctx, rww.written, o)

Expand Down Expand Up @@ -267,3 +269,20 @@ func WithRouteTag(route string, h http.Handler) http.Handler {
h.ServeHTTP(w, r)
})
}

// redactAttributes - return []keyValue with applied redaction to attributes if match key in redactedAttributes.
func (h *middleware) redactAttributes(attributes []attribute.KeyValue) []attribute.KeyValue {
var result []attribute.KeyValue
for _, att := range attributes {
result = append(result, h.redactAttribute(att))
}
return result
}

// redactAttribute - return keyValue with applied redaction to attributes if match key in redactedAttributes.
func (h *middleware) redactAttribute(keyValue attribute.KeyValue) attribute.KeyValue {
if _, ok := h.redactedAttributes[keyValue.Key]; ok {
return attribute.KeyValue{Key: keyValue.Key, Value: attribute.StringValue("****")}
}
return keyValue
}
43 changes: 43 additions & 0 deletions instrumentation/net/http/otelhttp/test/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"net/http/httptest"
"testing"

"go.opentelemetry.io/otel/attribute"

"github.com/stretchr/testify/assert"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
Expand Down Expand Up @@ -68,6 +70,47 @@ func TestBasicFilter(t *testing.T) {
}
}

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

spanRecorder := tracetest.NewSpanRecorder()
provider := trace.NewTracerProvider(trace.WithSpanProcessor(spanRecorder))
redactedKey := attribute.Key("http.method")

h := otelhttp.NewHandler(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := io.WriteString(w, "hello world"); err != nil {
t.Fatal(err)
}
}), "test_handler",
otelhttp.WithTracerProvider(provider),
otelhttp.WithRedactedAttributes(redactedKey),
)

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")
}
spans := spanRecorder.Ended()
if assert.Len(t, spans, 1) {
found := false
for _, a := range spans[0].Attributes() {
if a.Key == redactedKey {
found = true
assert.Equal(t, attribute.StringValue("****"), a.Value)
}
}
assert.True(t, found, "redacted key not found")
}
}

func TestSpanNameFormatter(t *testing.T) {
var testCases = []struct {
name string
Expand Down
37 changes: 29 additions & 8 deletions instrumentation/net/http/otelhttp/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"net/http"
"net/http/httptrace"

"go.opentelemetry.io/otel/attribute"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
Expand All @@ -32,12 +34,13 @@ import (
type Transport struct {
rt http.RoundTripper

tracer trace.Tracer
propagators propagation.TextMapPropagator
spanStartOptions []trace.SpanStartOption
filters []Filter
spanNameFormatter func(string, *http.Request) string
clientTrace func(context.Context) *httptrace.ClientTrace
tracer trace.Tracer
propagators propagation.TextMapPropagator
spanStartOptions []trace.SpanStartOption
filters []Filter
redactedAttributes map[attribute.Key]struct{}
spanNameFormatter func(string, *http.Request) string
clientTrace func(context.Context) *httptrace.ClientTrace
}

var _ http.RoundTripper = &Transport{}
Expand Down Expand Up @@ -74,6 +77,7 @@ func (t *Transport) applyConfig(c *config) {
t.filters = c.Filters
t.spanNameFormatter = c.SpanNameFormatter
t.clientTrace = c.ClientTrace
t.redactedAttributes = c.RedactedAttributes
}

func defaultTransportFormatter(_ string, r *http.Request) string {
Expand Down Expand Up @@ -110,7 +114,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
}

r = r.WithContext(ctx)
span.SetAttributes(semconvutil.HTTPClientRequest(r)...)
span.SetAttributes(t.redactAttributes(semconvutil.HTTPClientRequest(r))...)
t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header))

res, err := t.rt.RoundTrip(r)
Expand All @@ -121,7 +125,7 @@ func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
return res, err
}

span.SetAttributes(semconvutil.HTTPClientResponse(res)...)
span.SetAttributes(t.redactAttributes(semconvutil.HTTPClientResponse(res))...)
span.SetStatus(semconvutil.HTTPClientStatus(res.StatusCode))
res.Body = newWrappedBody(span, res.Body)

Expand Down Expand Up @@ -191,3 +195,20 @@ func (wb *wrappedBody) Close() error {
}
return nil
}

// redactAttributes - return []keyValue with applied redaction to attributes if match key in redactedAttributes.
func (t *Transport) redactAttributes(attributes []attribute.KeyValue) []attribute.KeyValue {
var result []attribute.KeyValue
for _, att := range attributes {
result = append(result, t.redactAttribute(att))
}
return result
}

// redactAttribute - return keyValue with applied redaction to attributes if match key in redactedAttributes.
func (t *Transport) redactAttribute(keyValue attribute.KeyValue) attribute.KeyValue {
if _, ok := t.redactedAttributes[keyValue.Key]; ok {
return attribute.KeyValue{Key: keyValue.Key, Value: attribute.StringValue("****")}
}
return keyValue
}

0 comments on commit 71b8100

Please sign in to comment.