From 0845028b70a41e58b70db503000326c361bc1a47 Mon Sep 17 00:00:00 2001 From: Nick Otter Date: Thu, 1 Oct 2020 09:13:10 -0700 Subject: [PATCH] Add FormatStatsPath to ochttp Transport to reduce stats path cardinality Updates ochttp.Transport to accept an optional FormatStatsPath which is supplied to ochttp.statsTransport. FormatStatsPath can override a request URL's path parameter to reduce cardinality for stats similar to FormatSpanName. --- plugin/ochttp/client.go | 12 ++++++++- plugin/ochttp/client_stats.go | 12 ++++++--- plugin/ochttp/example_test.go | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/plugin/ochttp/client.go b/plugin/ochttp/client.go index da815b2a7..241840d8a 100644 --- a/plugin/ochttp/client.go +++ b/plugin/ochttp/client.go @@ -56,6 +56,10 @@ type Transport struct { // name equals the URL Path. FormatSpanName func(*http.Request) string + // FormatStatsPath holds the function to use for standardizing the path + // supplied to metrics. By default the path equals the URL path. + FormatStatsPath func(*http.Request) string + // NewClientTrace may be set to a function allowing the current *trace.Span // to be annotated with HTTP request event information emitted by the // httptrace package. @@ -95,7 +99,13 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { formatSpanName: spanNameFormatter, newClientTrace: t.NewClientTrace, } - rt = statsTransport{base: rt} + + statsPathFormatter := t.FormatStatsPath + if statsPathFormatter == nil { + statsPathFormatter = statsPath + } + + rt = statsTransport{base: rt, pathFormatter: statsPathFormatter} return rt.RoundTrip(req) } diff --git a/plugin/ochttp/client_stats.go b/plugin/ochttp/client_stats.go index 17142aabe..f54e8787f 100644 --- a/plugin/ochttp/client_stats.go +++ b/plugin/ochttp/client_stats.go @@ -28,7 +28,8 @@ import ( // statsTransport is an http.RoundTripper that collects stats for the outgoing requests. type statsTransport struct { - base http.RoundTripper + base http.RoundTripper + pathFormatter func(*http.Request) string } // RoundTrip implements http.RoundTripper, delegating to Base and recording stats for the request. @@ -36,8 +37,8 @@ func (t statsTransport) RoundTrip(req *http.Request) (*http.Response, error) { ctx, _ := tag.New(req.Context(), tag.Upsert(KeyClientHost, req.Host), tag.Upsert(Host, req.Host), - tag.Upsert(KeyClientPath, req.URL.Path), - tag.Upsert(Path, req.URL.Path), + tag.Upsert(KeyClientPath, t.pathFormatter(req)), + tag.Upsert(Path, t.pathFormatter(req)), tag.Upsert(KeyClientMethod, req.Method), tag.Upsert(Method, req.Method)) req = req.WithContext(ctx) @@ -141,3 +142,8 @@ func (t *tracker) Close() error { t.end() return t.body.Close() } + +// statsPath gets the path from a request url +func statsPath(req *http.Request) string { + return req.URL.Path +} diff --git a/plugin/ochttp/example_test.go b/plugin/ochttp/example_test.go index bb115abb9..3069bc2b8 100644 --- a/plugin/ochttp/example_test.go +++ b/plugin/ochttp/example_test.go @@ -17,6 +17,7 @@ package ochttp_test import ( "log" "net/http" + "regexp" "go.opencensus.io/plugin/ochttp" "go.opencensus.io/plugin/ochttp/propagation/b3" @@ -54,6 +55,55 @@ func ExampleTransport() { _ = client } +// FormatStatsPath allows paths to be standardized before the tag is assigned +// +// For example https://jsonplaceholder.typicode.com/posts/1 and https://jsonplaceholder.typicode.com/posts/2 +// can have the path `/posts/:id` while https://jsonplaceholder.typicode.com/posts/1/comments has the path +// `/posts/:id/comments` +// +// Using FormatStatsPath can reduce cardinality when using KeyClientPath +func ExampleTransport_statsPathFormatter() { + // import ( + // "go.opencensus.io/plugin/ochttp" + // "go.opencensus.io/stats/view" + // ) + + // Add KeyClientPath and KeyClientHost tags to a few views. + ochttp.ClientRoundtripLatencyDistribution.TagKeys = append(ochttp.ClientRoundtripLatencyDistribution.TagKeys, ochttp.KeyClientPath, ochttp.KeyClientHost) + ochttp.ClientCompletedCount.TagKeys = append(ochttp.ClientCompletedCount.TagKeys, ochttp.KeyClientPath, ochttp.KeyClientHost) + + if err := view.Register( + // Register a few default views. + ochttp.ClientCompletedCount, + ochttp.ClientRoundtripLatencyDistribution, + ); err != nil { + log.Fatal(err) + } + + postRegexp := regexp.MustCompile(`^\/?posts\/[0-9]+\/?$`) + postCommentsRegexp := regexp.MustCompile(`^\/?posts\/[0-9]+\/comments\/?$`) + + client := &http.Client{ + Transport: &ochttp.Transport{ + FormatStatsPath: func(req *http.Request) string { + if req.URL.Host == "jsonplaceholder.typicode.com" { + if postRegexp.MatchString(req.URL.Path) { // format the post endpoint + return "/posts/:id" + } else if postCommentsRegexp.MatchString(req.URL.Path) { // format the post comment endpoint + return "/posts/:id/comments" + } + } + + // use the URL path as the default path + return req.URL.Path + }, + }, + } + + // Use client to perform requests. + _ = client +} + var usersHandler http.Handler func ExampleHandler() {