mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-01-12 02:28:07 +02:00
Add filters for othttp plugin (#556)
* Add request filtering capability to othhtp.Handler * Add simple and useful filters for othttp plugin * Add note that all requests are traced in the absence of any filters * Add copyright notice to plugin/othttp/filters/filters_test.go Co-Authored-By: Tyler Yahn <MrAlias@users.noreply.github.com> * Add package docstring for filters package Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com> Co-authored-by: Rahul Patel <rahulpa@google.com>
This commit is contained in:
parent
217a97d9b6
commit
2ef25ea570
154
plugin/othttp/filters/filters.go
Normal file
154
plugin/othttp/filters/filters.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2020, 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
|
||||
// othttp.WithFilter() option to control which inbound requests are traced.
|
||||
package filters
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/plugin/othttp"
|
||||
)
|
||||
|
||||
// Any takes a list of Filters and returns a Filter that
|
||||
// returns true if any Filter in the list returns true.
|
||||
func Any(fs ...othttp.Filter) othttp.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 ...othttp.Filter) othttp.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 ...othttp.Filter) othttp.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 othttp.Filter) othttp.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) othttp.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) othttp.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) othttp.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) othttp.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) othttp.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) othttp.Filter {
|
||||
return func(r *http.Request) bool {
|
||||
return m == r.Method
|
||||
}
|
||||
}
|
||||
|
||||
// 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) othttp.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) othttp.Filter {
|
||||
return func(r *http.Request) bool {
|
||||
for _, hv := range r.Header.Values(k) {
|
||||
if strings.Contains(hv, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
266
plugin/othttp/filters/filters_test.go
Normal file
266
plugin/othttp/filters/filters_test.go
Normal file
@ -0,0 +1,266 @@
|
||||
// Copyright 2020, 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"
|
||||
|
||||
"go.opentelemetry.io/otel/plugin/othttp"
|
||||
)
|
||||
|
||||
type scenario struct {
|
||||
name string
|
||||
filter othttp.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)
|
||||
}
|
||||
}
|
||||
}
|
@ -41,6 +41,10 @@ const (
|
||||
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
|
||||
// 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
|
||||
@ -54,6 +58,7 @@ type Handler struct {
|
||||
spanStartOptions []trace.StartOption
|
||||
readEvent bool
|
||||
writeEvent bool
|
||||
filters []Filter
|
||||
}
|
||||
|
||||
// Option function used for setting *optional* Handler properties
|
||||
@ -93,6 +98,18 @@ func WithSpanOptions(opts ...trace.StartOption) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -141,6 +158,14 @@ func NewHandler(handler http.Handler, operation string, opts ...Option) http.Han
|
||||
|
||||
// ServeHTTP serves HTTP requests (http.Handler)
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
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{}, h.spanStartOptions...) // start with the configured options
|
||||
|
||||
ctx := propagation.ExtractHTTP(r.Context(), h.props, r.Header)
|
||||
|
@ -59,3 +59,44 @@ func TestBasics(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user