Repository: goadesign/goa
Pull request #2273 (2019-09-05 08:46:10)

Fix collecting OpenAPI vendor extensions and tags in root level (#2271)
The API level vendor extension was ignored, so when I blame the code, it seemed that this change was the cause.

https://github.com/goadesign/goa/commit/6d9200ca48bab5261f3929cef8f114cf7d55a7ec#diff-6ecbb03fe70b40b52aa6080b1153f04aL53

Similarly for `swagger:tags`

https://github.com/goadesign/goa/commit/6d9200ca48bab5261f3929cef8f114cf7d55a7ec#diff-6ecbb03fe70b40b52aa6080b1153f04aL23


raphael: Thank you this is great! do you think you could add a test that would fail without this change?

Patch:
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 00000000..c9714211
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,18 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 60
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+  - pinned
+  - security
+  - bug
+# Label to use when marking an issue as stale
+staleLabel: wontfix
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+  This issue has been automatically marked as stale because it has not had
+  recent activity. It will be closed if no further activity occurs. Thank you
+  for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.travis.yml b/.travis.yml
index 43a6c3fe..29fa900e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,7 @@
 language: go
 dist: xenial
 go:
-- 1.12.x
+- 1.13.x
 install:
 - export PATH=${PATH}:${HOME}/gopath/bin
 script:
diff --git a/Makefile b/Makefile
index fc6f13ef..f95855e4 100644
--- a/Makefile
+++ b/Makefile
@@ -28,15 +28,13 @@ endif
 DEPEND=\
 	golang.org/x/lint/golint \
 	golang.org/x/tools/cmd/goimports \
-	github.com/cheggaaa/pb \
-	github.com/hashicorp/go-getter \
 	github.com/golang/protobuf/protoc-gen-go \
 	github.com/golang/protobuf/proto \
 	honnef.co/go/tools/cmd/staticcheck
 
 all: lint test
 
-travis: depend all test-examples test-plugins
+travis: depend all #test-examples test-plugins
 
 # Install protoc
 PROTOC_VERSION=3.6.1
@@ -56,12 +54,11 @@ PROTOC_EXEC="$(PROTOC)\bin\protoc.exe"
 endif
 depend:
 	@go get -v $(DEPEND)
-	@go install github.com/hashicorp/go-getter/cmd/go-getter && \
+	@env GO111MODULE=off go get github.com/hashicorp/go-getter/cmd/go-getter && \
 		go-getter https://github.com/google/protobuf/releases/download/v$(PROTOC_VERSION)/$(PROTOC).zip $(PROTOC) && \
 		cp $(PROTOC_EXEC) $(GOBIN) && \
 		rm -r $(PROTOC) && \
 		echo "`protoc --version`"
-	@go install github.com/golang/protobuf/protoc-gen-go
 	@go get -t -v ./...
 
 lint:
@@ -87,7 +84,7 @@ test-examples:
 	fi
 	@cd $(EXAMPLES_DIR) && git checkout $(GOA_BRANCH) || echo "Using master branch in examples repo" && \
 	make -k travis || (echo "Tests in examples repo (https://github.com/goadesign/examples) failed" \
-                  "due to changes in goa repo (branch: $(GOA_BRANCH))!" \
+                  "due to changes in Goa repo (branch: $(GOA_BRANCH))!" \
                   "Create a branch with name '$(GOA_BRANCH)' in the examples repo and fix these errors." && exit 1)
 
 test-plugins:
diff --git a/README.md b/README.md
index 24184dad..cb893d7b 100644
--- a/README.md
+++ b/README.md
@@ -108,7 +108,7 @@ without having to make changes.
 Releases are tagged with the corresponding version number. There is also a
 branch for each major version (`v1`, `v2` and `v3`).
 
-Current Release: `v3.0.4`
+Current Release: `v3.0.6`
 
 ## Teaser
 
diff --git a/RELEASE.md b/RELEASE.md
index 9e1aced0..06939ab7 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -6,8 +6,11 @@ This document is intended to help Goa maintainers release new versions of Goa.
 
 1. Update `pkg/version.go` and `README.md` to reflect the new version.
 2. Create git tags in the Goa repo for both the `v2` and `v3` branches.
-3. Generate and push the Goa examples from the `goadesign/examples` repo.
-4. Generate and push the plugin examples from the `goadesign/plugins` repo.
-5. Create git tag for plugin repo.
+3. Update `go.mod` in the examples repo `master` branch.
+4. Generate the Goa examples from the examples repo `master` and `v2` branches.
+5. Push the examples repo `master` and `v2` branches.
+6. Update the plugins repo `v3` branch `go.mod` file.
+6. Generate the plugin examples from the plugins repo `v3` and `v2` branches.
+5. Create git tags for plugin repo (both `v2` and `v3`).
 5. Push the tags.
 6. Write and publish blog to goa.design.
diff --git a/cmd/goa/main.go b/cmd/goa/main.go
index 9a8d2ec4..fcd2d0cd 100644
--- a/cmd/goa/main.go
+++ b/cmd/goa/main.go
@@ -24,7 +24,7 @@ func main() {
 
 		switch os.Args[1] {
 		case "version":
-			fmt.Println("goa version " + goa.Version())
+			fmt.Println("Goa version " + goa.Version())
 			os.Exit(0)
 		case "gen", "example":
 			if len(os.Args) == 2 {
diff --git a/codegen/example/example_cli.go b/codegen/example/example_cli.go
index e86428a9..308ecf22 100644
--- a/codegen/example/example_cli.go
+++ b/codegen/example/example_cli.go
@@ -132,7 +132,7 @@ const (
 						}
 					}
 					if !{{ .VarName }}Seen {
-						fmt.Fprintf(os.Stderr, "invalid value for URL '{{ .Name }}' variable: %q (valid values: {{ join .Values "," }})", *{{ .VarName }}F)
+						fmt.Fprintf(os.Stderr, "invalid value for URL '{{ .Name }}' variable: %q (valid values: {{ join .Values "," }})\n", *{{ .VarName }}F)
 						os.Exit(1)
 					}
 				{{- end }}
@@ -140,7 +140,7 @@ const (
 			{{- end }}
 		{{- end }}
 			default:
-				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: {{ join .Server.AvailableHosts "|" }})", *hostF)
+				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: {{ join .Server.AvailableHosts "|" }})\n", *hostF)
 				os.Exit(1)
 			}
 		}
@@ -155,7 +155,7 @@ const (
 	{
 		u, err := url.Parse(addr)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 			os.Exit(1)
 		}
 		scheme = u.Scheme
@@ -176,7 +176,7 @@ const (
 			endpoint, payload, err = do{{ toUpper $t.Name }}(scheme, host, timeout, debug)
 	{{- end }}
 		default:
-			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: {{ join .Server.Schemes "|" }})", scheme)
+			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: {{ join .Server.Schemes "|" }})\n", scheme)
 			os.Exit(1)
 		}
 	}
diff --git a/codegen/example/example_server.go b/codegen/example/example_server.go
index e47ab193..4463b436 100644
--- a/codegen/example/example_server.go
+++ b/codegen/example/example_server.go
@@ -255,7 +255,7 @@ func main() {
 						}
 					}
 					if !{{ .VarName }}Seen {
-						fmt.Fprintf(os.Stderr, "invalid value for URL '{{ .Name }}' variable: %q (valid values: {{ join .Values "," }})", *{{ .VarName }}F)
+						fmt.Fprintf(os.Stderr, "invalid value for URL '{{ .Name }}' variable: %q (valid values: {{ join .Values "," }})\n", *{{ .VarName }}F)
 						os.Exit(1)
 					}
 				{{- end }}
@@ -263,7 +263,7 @@ func main() {
 			{{- end }}
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -284,7 +284,7 @@ func main() {
 	{{ end }}
 {{- end }}
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: {{ join .Server.AvailableHosts "|" }})", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: {{ join .Server.AvailableHosts "|" }})\n", *hostF)
 	}
 `
 
diff --git a/codegen/generator/generate.go b/codegen/generator/generate.go
index 01e7a073..6fc44a7a 100644
--- a/codegen/generator/generate.go
+++ b/codegen/generator/generate.go
@@ -1,6 +1,7 @@
 package generator
 
 import (
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"sort"
@@ -33,7 +34,18 @@ func Generate(dir, cmd string) ([]string, error) {
 		if err := os.MkdirAll(path, 0777); err != nil {
 			return nil, err
 		}
-		pkgs, err := packages.Load(nil, path)
+
+		// We create a temporary Go file to make sure the directory is a valid Go package
+		dummy, err := ioutil.TempFile(path, "temp.*.go")
+		if err != nil {
+			return nil, err
+		}
+		defer os.Remove(dummy.Name())
+		if err = ioutil.WriteFile(dummy.Name(), []byte("package gen"), 0644); err != nil {
+			return nil, err
+		}
+
+		pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName}, path)
 		if err != nil {
 			return nil, err
 		}
diff --git a/dsl/http.go b/dsl/http.go
index d51fe16a..2611b262 100644
--- a/dsl/http.go
+++ b/dsl/http.go
@@ -699,6 +699,10 @@ func Body(args ...interface{}) {
 // Parent sets the name of the parent service. The parent service canonical
 // method path is used as prefix for all the service HTTP endpoint paths.
 //
+// Attributes of the parent method payload that map to parent path parameters
+// are automatically merged into the child method payload type if not already
+// defined.
+//
 // Parent must appear in a Service expression.
 //
 // Parent accepts one argument: the name of the parent service.
diff --git a/dsl/meta.go b/dsl/meta.go
index b7bf68ce..0bcc7394 100644
--- a/dsl/meta.go
+++ b/dsl/meta.go
@@ -150,8 +150,6 @@ func Meta(name string, value ...string) {
 	}
 
 	switch e := eval.Current().(type) {
-	case *expr.RootExpr:
-		e.Meta = appendMeta(e.Meta, name, value...)
 	case *expr.APIExpr:
 		e.Meta = appendMeta(e.Meta, name, value...)
 	case *expr.AttributeExpr:
diff --git a/expr/http_endpoint.go b/expr/http_endpoint.go
index 9040bfe1..d74f9b34 100644
--- a/expr/http_endpoint.go
+++ b/expr/http_endpoint.go
@@ -176,7 +176,19 @@ func (e *HTTPEndpointExpr) Prepare() {
 		if c := p.CanonicalEndpoint(); c != nil {
 			if !e.HasAbsoluteRoutes() {
 				headers.Merge(c.Headers)
-				params.Merge(c.Params)
+				params.Merge(c.PathParams())
+
+				// Inherit attributes for path params from parent service
+				WalkMappedAttr(c.PathParams(), func(name, _ string, _ *AttributeExpr) error {
+					if att := c.MethodExpr.Payload.Find(name); att != nil {
+						if o := AsObject(e.MethodExpr.Payload.Type); o != nil {
+							if o.Attribute(name) == nil {
+								o.Set(name, att)
+							}
+						}
+					}
+					return nil
+				})
 			}
 		}
 	}
diff --git a/expr/root.go b/expr/root.go
index 337fa60d..daf70800 100644
--- a/expr/root.go
+++ b/expr/root.go
@@ -32,9 +32,6 @@ type (
 		Creations []*TypeMap
 		// Schemes list the registered security schemes.
 		Schemes []*SchemeExpr
-		// Meta is a set of key/value pairs with semantic that is
-		// specific to each generator.
-		Meta MetaExpr
 	}
 
 	// MetaExpr is a set of key/value pairs
diff --git a/grpc/middleware/trace.go b/grpc/middleware/trace.go
index 465bd771..8175f654 100644
--- a/grpc/middleware/trace.go
+++ b/grpc/middleware/trace.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"context"
+	"regexp"
 
 	"goa.design/goa/v3/middleware"
 	"google.golang.org/grpc"
@@ -36,7 +37,7 @@ const (
 func UnaryServerTrace(opts ...middleware.TraceOption) grpc.UnaryServerInterceptor {
 	o := middleware.NewTraceOptions(opts...)
 	return grpc.UnaryServerInterceptor(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
-		ctx = withTrace(ctx, o)
+		ctx = withTrace(ctx, info.FullMethod, o)
 		return handler(ctx, req)
 	})
 }
@@ -55,7 +56,7 @@ func UnaryServerTrace(opts ...middleware.TraceOption) grpc.UnaryServerIntercepto
 func StreamServerTrace(opts ...middleware.TraceOption) grpc.StreamServerInterceptor {
 	o := middleware.NewTraceOptions(opts...)
 	return grpc.StreamServerInterceptor(func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
-		ctx := withTrace(ss.Context(), o)
+		ctx := withTrace(ss.Context(), info.FullMethod, o)
 		wss := NewWrappedServerStream(ctx, ss)
 		return handler(srv, wss)
 	})
@@ -112,8 +113,14 @@ func SampleSize(s int) middleware.TraceOption {
 	return middleware.SampleSize(s)
 }
 
+// DiscardFromTrace adds a regular expression for matching a request path to be discarded from tracing.
+// see middleware.DiscardFromTrace() for more details.
+func DiscardFromTrace(discard *regexp.Regexp) middleware.TraceOption {
+	return middleware.DiscardFromTrace(discard)
+}
+
 // withTrace sets the trace ID, span ID, and parent span ID in the context.
-func withTrace(ctx context.Context, opts *middleware.TraceOptions) context.Context {
+func withTrace(ctx context.Context, fullMethod string, opts *middleware.TraceOptions) context.Context {
 	sampler := opts.NewSampler()
 	md, ok := metadata.FromIncomingContext(ctx)
 	if !ok {
@@ -123,9 +130,18 @@ func withTrace(ctx context.Context, opts *middleware.TraceOptions) context.Conte
 	var traceID string
 	{
 		traceID = MetadataValue(md, TraceIDMetadataKey)
-		if traceID == "" && sampler.Sample() {
-			// insert tracing only within sample.
-			traceID = opts.TraceID()
+		if traceID == "" {
+			var discarded bool
+			for _, discard := range opts.Discards() {
+				if discard.MatchString(fullMethod) {
+					discarded = true
+					break
+				}
+			}
+			if !discarded && sampler.Sample() {
+				// insert tracing only within sample.
+				traceID = opts.TraceID()
+			}
 		}
 	}
 	if traceID == "" {
diff --git a/http/codegen/example_server.go b/http/codegen/example_server.go
index ea25fb2e..c403754c 100644
--- a/http/codegen/example_server.go
+++ b/http/codegen/example_server.go
@@ -206,7 +206,7 @@ func handleHTTPServer(ctx context.Context, u *url.URL{{ range $.Services }}{{ if
 	// Provide the transport specific request decoder and response encoder.
 	// The goa http package has built-in support for JSON, XML and gob.
 	// Other encodings can be used by providing the corresponding functions,
-	// see goa.design/encoding.
+	// see goa.design/implement/encoding.
 	var (
 		dec = goahttp.RequestDecoder
 		enc = goahttp.ResponseEncoder
diff --git a/http/codegen/openapi/openapi_v2_builder.go b/http/codegen/openapi/openapi_v2_builder.go
index 5850d509..8a0b8115 100644
--- a/http/codegen/openapi/openapi_v2_builder.go
+++ b/http/codegen/openapi/openapi_v2_builder.go
@@ -18,7 +18,7 @@ func NewV2(root *expr.RootExpr, h *expr.HostExpr) (*V2, error) {
 	if root == nil {
 		return nil, nil
 	}
-	tags := tagsFromExpr(root.Meta)
+	tags := tagsFromExpr(root.API.Meta)
 	u, err := url.Parse(defaultURI(h))
 	if err != nil {
 		// This should never happen because server expression must have been
@@ -48,7 +48,7 @@ func NewV2(root *expr.RootExpr, h *expr.HostExpr) (*V2, error) {
 			Contact:        root.API.Contact,
 			License:        root.API.License,
 			Version:        root.API.Version,
-			Extensions:     ExtensionsFromExpr(root.Meta),
+			Extensions:     ExtensionsFromExpr(root.API.Meta),
 		},
 		Host:                host,
 		BasePath:            basePath,
@@ -325,8 +325,6 @@ func tagsFromExpr(mdata expr.MetaExpr) (tags []*Tag) {
 			tag.ExternalDocs = docs
 		}
 
-		tag.Extensions = ExtensionsFromExpr(mdata)
-
 		tags = append(tags, tag)
 	}
 
diff --git a/http/encoding.go b/http/encoding.go
index b964ec6a..8d05cb52 100644
--- a/http/encoding.go
+++ b/http/encoding.go
@@ -294,7 +294,7 @@ func (e *textDecoder) Decode(v interface{}) error {
 	case *string:
 		*c = string(b)
 	case *[]byte:
-		*c = []byte(string(b))
+		*c = b
 	default:
 		err = fmt.Errorf("can't decode %s to %T", e.ct, c)
 	}
diff --git a/http/middleware/debug.go b/http/middleware/debug.go
index de32ff61..53208f68 100644
--- a/http/middleware/debug.go
+++ b/http/middleware/debug.go
@@ -1,12 +1,14 @@
 package middleware
 
 import (
+	"bufio"
 	"bytes"
 	"crypto/rand"
 	"encoding/base64"
 	"fmt"
 	"io"
 	"io/ioutil"
+	"net"
 	"net/http"
 	"sort"
 	"strings"
@@ -116,6 +118,14 @@ func (r *responseDupper) WriteHeader(s int) {
 	r.ResponseWriter.WriteHeader(s)
 }
 
+// Hijack supports the http.Hijacker interface.
+func (r *responseDupper) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	if hijacker, ok := r.ResponseWriter.(http.Hijacker); ok {
+		return hijacker.Hijack()
+	}
+	return nil, nil, fmt.Errorf("debug middleware: inner ResponseWriter cannot be hijacked: %T", r.ResponseWriter)
+}
+
 // shortID produces a " unique" 6 bytes long string.
 // Do not use as a reliable way to get unique IDs, instead use for things like logging.
 func shortID() string {
diff --git a/http/middleware/trace.go b/http/middleware/trace.go
index ec74af1f..cddeda05 100644
--- a/http/middleware/trace.go
+++ b/http/middleware/trace.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"net/http"
+	"regexp"
 
 	"goa.design/goa/v3/middleware"
 )
@@ -38,9 +39,21 @@ func Trace(opts ...middleware.TraceOption) func(http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			// insert a new trace ID only if not already being traced.
 			traceID := r.Header.Get(TraceIDHeader)
-			if traceID == "" && sampler.Sample() {
-				// insert tracing only within sample.
-				traceID = o.TraceID()
+			if traceID == "" {
+				// check for discards only if we do not already have a trace ID and before sampling.
+				var discarded bool
+				if r.URL != nil { // docs imply but do not actually state that URL cannot be nil
+					for _, discard := range o.Discards() {
+						if discard.MatchString(r.URL.Path) {
+							discarded = true
+							break
+						}
+					}
+				}
+				if !discarded && sampler.Sample() {
+					// insert tracing only within sample.
+					traceID = o.TraceID()
+				}
 			}
 			if traceID == "" {
 				h.ServeHTTP(w, r)
@@ -80,6 +93,12 @@ func SampleSize(s int) middleware.TraceOption {
 	return middleware.SampleSize(s)
 }
 
+// DiscardFromTrace adds a regular expression for matching a request path to be discarded from tracing.
+// see middleware.DiscardFromTrace() for more details.
+func DiscardFromTrace(discard *regexp.Regexp) middleware.TraceOption {
+	return middleware.DiscardFromTrace(discard)
+}
+
 // WrapDoer wraps a goa client Doer and sets the trace headers so that the
 // downstream service may properly retrieve the parent span ID and trace ID.
 func WrapDoer(doer Doer) Doer {
diff --git a/middleware/trace.go b/middleware/trace.go
index 4c8e955f..7f279abb 100644
--- a/middleware/trace.go
+++ b/middleware/trace.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"context"
+	"regexp"
 )
 
 type (
@@ -20,6 +21,7 @@ type (
 		samplingPercent int
 		maxSamplingRate int
 		sampleSize      int
+		discards        []*regexp.Regexp
 	}
 
 	// tracedLogger is a logger which logs the trace ID with every log entry
@@ -67,6 +69,12 @@ func (o *TraceOptions) SpanID() string {
 	return o.spanIDFunc()
 }
 
+// Discards returns the list of regular expressions used to match paths to be
+// discarded from tracing.
+func (o *TraceOptions) Discards() []*regexp.Regexp {
+	return o.discards
+}
+
 // TraceIDFunc configures the function used to compute trace IDs. Use this
 // option to generate IDs compatible with backend tracing systems
 // (e.g. AWS XRay).
@@ -128,6 +136,22 @@ func SampleSize(s int) TraceOption {
 	}
 }
 
+// DiscardFromTrace adds a regular expression for matching a request path to be discarded from tracing.
+// this is useful for frequent API calls that are not important to trace, such as health checks.
+// the pattern can be a full or partial match and could even support both HTTP and gRPC paths with an
+// OR expression, etc.
+//
+// note that discards can be overridden if the incoming request already has a trace ID.
+func DiscardFromTrace(discard *regexp.Regexp) TraceOption {
+	if discard == nil {
+		panic("discard cannot be nil")
+	}
+	return func(o *TraceOptions) *TraceOptions {
+		o.discards = append(o.discards, discard)
+		return o
+	}
+}
+
 // WithSpan returns a context containing the given trace, span and parent span
 // IDs.
 func WithSpan(ctx context.Context, traceID, spanID, parentID string) context.Context {
diff --git a/pkg/version.go b/pkg/version.go
index 07b95f8a..fc0228e9 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -12,7 +12,7 @@ const (
 	// Minor version number
 	Minor = 0
 	// Build number
-	Build = 4
+	Build = 6
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/codegen/example/testdata/example_cli_code.go b/codegen/example/testdata/example_cli_code.go
index c4699011..b32a18c6 100644
--- a/codegen/example/testdata/example_cli_code.go
+++ b/codegen/example/testdata/example_cli_code.go
@@ -24,7 +24,7 @@ const (
 			case "localhost":
 				addr = "http://localhost:80"
 			default:
-				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: localhost)", *hostF)
+				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: localhost)\n", *hostF)
 				os.Exit(1)
 			}
 		}
@@ -39,7 +39,7 @@ const (
 	{
 		u, err := url.Parse(addr)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 			os.Exit(1)
 		}
 		scheme = u.Scheme
@@ -57,7 +57,7 @@ const (
 		case "grpc", "grpcs":
 			endpoint, payload, err = doGRPC(scheme, host, timeout, debug)
 		default:
-			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: grpc|http)", scheme)
+			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: grpc|http)\n", scheme)
 			os.Exit(1)
 		}
 	}
@@ -134,7 +134,7 @@ func indent(s string) string {
 			case "dev":
 				addr = "http://example:8090"
 			default:
-				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)", *hostF)
+				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)\n", *hostF)
 				os.Exit(1)
 			}
 		}
@@ -149,7 +149,7 @@ func indent(s string) string {
 	{
 		u, err := url.Parse(addr)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 			os.Exit(1)
 		}
 		scheme = u.Scheme
@@ -167,7 +167,7 @@ func indent(s string) string {
 		case "grpc", "grpcs":
 			endpoint, payload, err = doGRPC(scheme, host, timeout, debug)
 		default:
-			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: grpc|http|https)", scheme)
+			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: grpc|http|https)\n", scheme)
 			os.Exit(1)
 		}
 	}
@@ -262,7 +262,7 @@ func indent(s string) string {
 				addr = strings.Replace(addr, "{float64}", *float64_F, -1)
 				addr = strings.Replace(addr, "{bool}", *bool_F, -1)
 			default:
-				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)", *hostF)
+				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)\n", *hostF)
 				os.Exit(1)
 			}
 		}
@@ -277,7 +277,7 @@ func indent(s string) string {
 	{
 		u, err := url.Parse(addr)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 			os.Exit(1)
 		}
 		scheme = u.Scheme
@@ -293,7 +293,7 @@ func indent(s string) string {
 		case "http", "https":
 			endpoint, payload, err = doHTTP(scheme, host, timeout, debug)
 		default:
-			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: http|https)", scheme)
+			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: http|https)\n", scheme)
 			os.Exit(1)
 		}
 	}
@@ -381,7 +381,7 @@ func indent(s string) string {
 			case "stage":
 				addr = "https://example"
 			default:
-				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)", *hostF)
+				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)\n", *hostF)
 				os.Exit(1)
 			}
 		}
@@ -396,7 +396,7 @@ func indent(s string) string {
 	{
 		u, err := url.Parse(addr)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 			os.Exit(1)
 		}
 		scheme = u.Scheme
@@ -412,7 +412,7 @@ func indent(s string) string {
 		case "http", "https":
 			endpoint, payload, err = doHTTP(scheme, host, timeout, debug)
 		default:
-			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: http|https)", scheme)
+			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: http|https)\n", scheme)
 			os.Exit(1)
 		}
 	}
@@ -501,7 +501,7 @@ func indent(s string) string {
 					}
 				}
 				if !versionSeen {
-					fmt.Fprintf(os.Stderr, "invalid value for URL 'version' variable: %q (valid values: v1,v2)", *versionF)
+					fmt.Fprintf(os.Stderr, "invalid value for URL 'version' variable: %q (valid values: v1,v2)\n", *versionF)
 					os.Exit(1)
 				}
 				addr = strings.Replace(addr, "{version}", *versionF, -1)
@@ -510,7 +510,7 @@ func indent(s string) string {
 				addr = strings.Replace(addr, "{domain}", *domainF, -1)
 				addr = strings.Replace(addr, "{port}", *portF, -1)
 			default:
-				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)", *hostF)
+				fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)\n", *hostF)
 				os.Exit(1)
 			}
 		}
@@ -525,7 +525,7 @@ func indent(s string) string {
 	{
 		u, err := url.Parse(addr)
 		if err != nil {
-			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+			fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 			os.Exit(1)
 		}
 		scheme = u.Scheme
@@ -541,7 +541,7 @@ func indent(s string) string {
 		case "http", "https":
 			endpoint, payload, err = doHTTP(scheme, host, timeout, debug)
 		default:
-			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: http|https)", scheme)
+			fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: http|https)\n", scheme)
 			os.Exit(1)
 		}
 	}
diff --git a/codegen/example/testdata/example_server_code.go b/codegen/example/testdata/example_server_code.go
index 6e3a172a..d19bfa18 100644
--- a/codegen/example/testdata/example_server_code.go
+++ b/codegen/example/testdata/example_server_code.go
@@ -61,7 +61,7 @@ const (
 			addr := "http://localhost:80"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -83,7 +83,7 @@ const (
 			addr := "grpc://localhost:8080"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -102,7 +102,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: localhost)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: localhost)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -176,7 +176,7 @@ const (
 			addr := "http://localhost:80"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -198,7 +198,7 @@ const (
 			addr := "grpc://localhost:8080"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -217,7 +217,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: localhost)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: localhost)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -291,7 +291,7 @@ const (
 			addr := "http://example:8090"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -313,7 +313,7 @@ const (
 			addr := "https://example:80"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -335,7 +335,7 @@ const (
 			addr := "grpc://example:8080"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -354,7 +354,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -445,7 +445,7 @@ const (
 			addr = strings.Replace(addr, "{bool}", *bool_F, -1)
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -476,7 +476,7 @@ const (
 			addr = strings.Replace(addr, "{bool}", *bool_F, -1)
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -495,7 +495,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -551,7 +551,7 @@ const (
 			addr := "http://localhost:80"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -570,7 +570,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: svc)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: svc)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -644,7 +644,7 @@ const (
 			addr := "http://example:8090"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -666,7 +666,7 @@ const (
 			addr := "grpc://localhost:8080"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -685,7 +685,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -763,7 +763,7 @@ const (
 			addr := "http://example:8090"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -785,7 +785,7 @@ const (
 			addr := "grpc://localhost:8080"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -804,7 +804,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -877,7 +877,7 @@ const (
 			addr := "http://example:8090"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -900,7 +900,7 @@ const (
 			addr := "https://example"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -919,7 +919,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -1003,13 +1003,13 @@ const (
 				}
 			}
 			if !versionSeen {
-				fmt.Fprintf(os.Stderr, "invalid value for URL 'version' variable: %q (valid values: v1,v2)", *versionF)
+				fmt.Fprintf(os.Stderr, "invalid value for URL 'version' variable: %q (valid values: v1,v2)\n", *versionF)
 				os.Exit(1)
 			}
 			addr = strings.Replace(addr, "{version}", *versionF, -1)
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -1034,7 +1034,7 @@ const (
 			addr = strings.Replace(addr, "{port}", *portF, -1)
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -1053,7 +1053,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev|stage)\n", *hostF)
 	}
 
 	// Wait for signal.
@@ -1126,7 +1126,7 @@ const (
 			addr := "http://localhost:80"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -1148,7 +1148,7 @@ const (
 			addr := "grpc://localhost:8080"
 			u, err := url.Parse(addr)
 			if err != nil {
-				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s", addr, err)
+				fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err)
 				os.Exit(1)
 			}
 			if *secureF {
@@ -1167,7 +1167,7 @@ const (
 		}
 
 	default:
-		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: svc)", *hostF)
+		fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: svc)\n", *hostF)
 	}
 
 	// Wait for signal.
diff --git a/expr/http_endpoint_test.go b/expr/http_endpoint_test.go
index 71d9bc27..f89afc6c 100644
--- a/expr/http_endpoint_test.go
+++ b/expr/http_endpoint_test.go
@@ -60,6 +60,9 @@ func TestHTTPEndpointValidation(t *testing.T) {
 		"endpoint-missing-token-extend": {
 			DSL: testdata.EndpointExtendToken,
 		},
+		"endpoint-has-parent": {
+			DSL: testdata.EndpointHasParent,
+		},
 	}
 	for name, c := range cases {
 		t.Run(name, func(t *testing.T) {
diff --git a/expr/testdata/endpoint_dsls.go b/expr/testdata/endpoint_dsls.go
index ec63beac..99f848f2 100644
--- a/expr/testdata/endpoint_dsls.go
+++ b/expr/testdata/endpoint_dsls.go
@@ -150,6 +150,39 @@ var EndpointExtendToken = func() {
 	})
 }
 
+var EndpointHasParent = func() {
+	Service("Parent", func() {
+		HTTP(func() {
+			Path("/parents")
+			CanonicalMethod("Method")
+		})
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("parent_id", Int)
+				Attribute("query_1", String)
+			})
+			HTTP(func() {
+				GET("/{parent_id}")
+				Param("query_1")
+			})
+		})
+	})
+	Service("Child", func() {
+		HTTP(func() {
+			Path("/children")
+			Parent("Parent")
+		})
+		Method("Method", func() {
+			Payload(func() {
+				Attribute("child_id", Int)
+			})
+			HTTP(func() {
+				GET("/{child_id}")
+			})
+		})
+	})
+}
+
 var FinalizeEndpointBodyAsExtendedTypeDSL = func() {
 	var EntityData = Type("EntityData", func() {
 		Attribute("name", String)
diff --git a/grpc/middleware/trace_test.go b/grpc/middleware/trace_test.go
index 923a2fbb..6fe58c4a 100644
--- a/grpc/middleware/trace_test.go
+++ b/grpc/middleware/trace_test.go
@@ -3,6 +3,7 @@ package middleware_test
 import (
 	"context"
 	"fmt"
+	"regexp"
 	"testing"
 
 	grpcm "goa.design/goa/v3/grpc/middleware"
@@ -16,6 +17,7 @@ var (
 	spanID     = "testSpanID"
 	newTraceID = func() string { return traceID }
 	newID      = func() string { return spanID }
+	discard    = regexp.MustCompile("Test$")
 )
 
 func TestUnaryServerTrace(t *testing.T) {
@@ -26,16 +28,22 @@ func TestUnaryServerTrace(t *testing.T) {
 	cases := map[string]struct {
 		Rate                  int
 		TraceID, ParentSpanID string
+		Discard               *regexp.Regexp
 		// output
 		CtxTraceID, CtxSpanID, CtxParentID string
 	}{
-		"no-trace": {100, "", "", traceID, spanID, ""},
-		"trace":    {100, "trace", "", "trace", spanID, ""},
-		"parent":   {100, "trace", "parent", "trace", spanID, "parent"},
+		"no-trace":             {100, "", "", nil, traceID, spanID, ""},
+		"no-trace-discarded":   {100, "", "", discard, "", "", ""},
+		"trace":                {100, "trace", "", nil, "trace", spanID, ""},
+		"trace-not-discarded":  {100, "trace", "", discard, "trace", spanID, ""},
+		"parent":               {100, "trace", "parent", nil, "trace", spanID, "parent"},
+		"parent-not-discarded": {100, "trace", "parent", discard, "trace", spanID, "parent"},
 
-		"zero-rate-no-trace": {0, "", "", "", "", ""},
-		"zero-rate-trace":    {0, "trace", "", "trace", spanID, ""},
-		"zero-rate-parent":   {0, "trace", "parent", "trace", spanID, "parent"},
+		"zero-rate-no-trace":             {0, "", "", nil, "", "", ""},
+		"zero-rate-trace":                {0, "trace", "", nil, "trace", spanID, ""},
+		"zero-rate-trace-not-discarded":  {0, "trace", "", discard, "trace", spanID, ""},
+		"zero-rate-parent":               {0, "trace", "parent", nil, "trace", spanID, "parent"},
+		"zero-rate-parent-not-discarded": {0, "trace", "parent", discard, "trace", spanID, "parent"},
 	}
 	for k, c := range cases {
 		t.Run(k, func(t *testing.T) {
@@ -72,11 +80,15 @@ func TestUnaryServerTrace(t *testing.T) {
 				md.Set(grpcm.ParentSpanIDMetadataKey, c.ParentSpanID)
 			}
 			ctx := metadata.NewIncomingContext(context.Background(), md)
-
-			_, err := grpcm.UnaryServerTrace(
+			traceOptions := []middleware.TraceOption{
 				grpcm.SamplingPercent(c.Rate),
 				grpcm.SpanIDFunc(newID),
-				grpcm.TraceIDFunc(newTraceID))(ctx, "request", unary, handler)
+				grpcm.TraceIDFunc(newTraceID),
+			}
+			if c.Discard != nil {
+				traceOptions = append(traceOptions, grpcm.DiscardFromTrace(c.Discard))
+			}
+			_, err := grpcm.UnaryServerTrace(traceOptions...)(ctx, "request", unary, handler)
 			if err != nil {
 				t.Errorf("UnaryServerTrace error: %v", err)
 			}
@@ -92,16 +104,22 @@ func TestStreamServerTrace(t *testing.T) {
 	cases := map[string]struct {
 		Rate                  int
 		TraceID, ParentSpanID string
+		Discard               *regexp.Regexp
 		// output
 		CtxTraceID, CtxSpanID, CtxParentID string
 	}{
-		"no-trace": {100, "", "", traceID, spanID, ""},
-		"trace":    {100, "trace", "", "trace", spanID, ""},
-		"parent":   {100, "trace", "parent", "trace", spanID, "parent"},
+		"no-trace":             {100, "", "", nil, traceID, spanID, ""},
+		"no-trace-discarded":   {100, "", "", discard, "", "", ""},
+		"trace":                {100, "trace", "", nil, "trace", spanID, ""},
+		"trace-not-discarded":  {100, "trace", "", discard, "trace", spanID, ""},
+		"parent":               {100, "trace", "parent", nil, "trace", spanID, "parent"},
+		"parent-not-discarded": {100, "trace", "parent", discard, "trace", spanID, "parent"},
 
-		"zero-rate-no-trace": {0, "", "", "", "", ""},
-		"zero-rate-trace":    {0, "trace", "", "trace", spanID, ""},
-		"zero-rate-parent":   {0, "trace", "parent", "trace", spanID, "parent"},
+		"zero-rate-no-trace":             {0, "", "", nil, "", "", ""},
+		"zero-rate-trace":                {0, "trace", "", nil, "trace", spanID, ""},
+		"zero-rate-trace-not-discarded":  {0, "trace", "", discard, "trace", spanID, ""},
+		"zero-rate-parent":               {0, "trace", "parent", nil, "trace", spanID, "parent"},
+		"zero-rate-parent-not-discarded": {0, "trace", "parent", discard, "trace", spanID, "parent"},
 	}
 	for k, c := range cases {
 		t.Run(k, func(t *testing.T) {
@@ -140,11 +158,15 @@ func TestStreamServerTrace(t *testing.T) {
 			}
 			ctx := metadata.NewIncomingContext(context.Background(), md)
 			wss := grpcm.NewWrappedServerStream(ctx, &testServerStream{})
-
-			err := grpcm.StreamServerTrace(
+			traceOptions := []middleware.TraceOption{
 				grpcm.SamplingPercent(c.Rate),
 				grpcm.SpanIDFunc(newID),
-				grpcm.TraceIDFunc(newTraceID))(nil, wss, stream, handler)
+				grpcm.TraceIDFunc(newTraceID),
+			}
+			if c.Discard != nil {
+				traceOptions = append(traceOptions, grpcm.DiscardFromTrace(c.Discard))
+			}
+			err := grpcm.StreamServerTrace(traceOptions...)(nil, wss, stream, handler)
 			if err != nil {
 				t.Errorf("StreamServerTrace error: %v", err)
 			}
diff --git a/http/codegen/testdata/example_code.go b/http/codegen/testdata/example_code.go
index 2d98cd01..6d0f999e 100644
--- a/http/codegen/testdata/example_code.go
+++ b/http/codegen/testdata/example_code.go
@@ -16,7 +16,7 @@ func handleHTTPServer(ctx context.Context, u *url.URL, serviceEndpoints *service
 	// Provide the transport specific request decoder and response encoder.
 	// The goa http package has built-in support for JSON, XML and gob.
 	// Other encodings can be used by providing the corresponding functions,
-	// see goa.design/encoding.
+	// see goa.design/implement/encoding.
 	var (
 		dec = goahttp.RequestDecoder
 		enc = goahttp.ResponseEncoder
@@ -109,7 +109,7 @@ func handleHTTPServer(ctx context.Context, u *url.URL, wg *sync.WaitGroup, errc
 	// Provide the transport specific request decoder and response encoder.
 	// The goa http package has built-in support for JSON, XML and gob.
 	// Other encodings can be used by providing the corresponding functions,
-	// see goa.design/encoding.
+	// see goa.design/implement/encoding.
 	var (
 		dec = goahttp.RequestDecoder
 		enc = goahttp.ResponseEncoder
@@ -202,7 +202,7 @@ func handleHTTPServer(ctx context.Context, u *url.URL, serviceEndpoints *service
 	// Provide the transport specific request decoder and response encoder.
 	// The goa http package has built-in support for JSON, XML and gob.
 	// Other encodings can be used by providing the corresponding functions,
-	// see goa.design/encoding.
+	// see goa.design/implement/encoding.
 	var (
 		dec = goahttp.RequestDecoder
 		enc = goahttp.ResponseEncoder
@@ -295,7 +295,7 @@ func handleHTTPServer(ctx context.Context, u *url.URL, serviceEndpoints *service
 	// Provide the transport specific request decoder and response encoder.
 	// The goa http package has built-in support for JSON, XML and gob.
 	// Other encodings can be used by providing the corresponding functions,
-	// see goa.design/encoding.
+	// see goa.design/implement/encoding.
 	var (
 		dec = goahttp.RequestDecoder
 		enc = goahttp.ResponseEncoder
@@ -394,7 +394,7 @@ func handleHTTPServer(ctx context.Context, u *url.URL, streamingServiceAEndpoint
 	// Provide the transport specific request decoder and response encoder.
 	// The goa http package has built-in support for JSON, XML and gob.
 	// Other encodings can be used by providing the corresponding functions,
-	// see goa.design/encoding.
+	// see goa.design/implement/encoding.
 	var (
 		dec = goahttp.RequestDecoder
 		enc = goahttp.ResponseEncoder
diff --git a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden
index 5f6a398e..3818bd84 100644
--- a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden
+++ b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file0.golden
@@ -1,1 +1,1 @@
-{"swagger":"2.0","info":{"title":"","version":""},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"operationId":"testService#testEndpoint","responses":{"204":{"description":"No Content response."}},"schemes":["https"],"summary":"testEndpoint testService","tags":["testService"]},"x-test-foo":"bar"}}}
\ No newline at end of file
+{"swagger":"2.0","info":{"title":"","version":"","x-test-api":"API"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"operationId":"testService#testEndpoint","responses":{"204":{"description":"No Content response."}},"schemes":["https"],"summary":"testEndpoint testService","tags":["testService"]},"x-test-foo":"bar"}},"tags":[{"name":"Backend","description":"Description of Backend","externalDocs":{"description":"See more docs here","url":"http://example.com"}}]}
\ No newline at end of file
diff --git a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden
index eced3f34..10b5b68d 100644
--- a/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden
+++ b/http/codegen/testdata/openapi_v2/TestExtensions/endpoint_file1.golden
@@ -2,6 +2,7 @@ swagger: "2.0"
 info:
   title: ""
   version: ""
+  x-test-api: API
 host: goa.design
 consumes:
 - application/json
@@ -24,3 +25,9 @@ paths:
       tags:
       - testService
     x-test-foo: bar
+tags:
+- name: Backend
+  description: Description of Backend
+  externalDocs:
+    description: See more docs here
+    url: http://example.com
diff --git a/http/codegen/testdata/openapi_v2_dsls.go b/http/codegen/testdata/openapi_v2_dsls.go
index ec582f3f..8c8851a5 100644
--- a/http/codegen/testdata/openapi_v2_dsls.go
+++ b/http/codegen/testdata/openapi_v2_dsls.go
@@ -273,6 +273,11 @@ var ExtensionDSL = func() {
 				URI("https://goa.design")
 			})
 		})
+		Meta("swagger:extension:x-test-api", "API")
+		Meta("swagger:tag:Backend")
+		Meta("swagger:tag:Backend:desc", "Description of Backend")
+		Meta("swagger:tag:Backend:url", "http://example.com")
+		Meta("swagger:tag:Backend:url:desc", "See more docs here")
 	})
 	Service("testService", func() {
 		Method("testEndpoint", func() {
diff --git a/http/middleware/trace_test.go b/http/middleware/trace_test.go
index 67c979e8..0f7bc1a9 100644
--- a/http/middleware/trace_test.go
+++ b/http/middleware/trace_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"net/http"
 	"net/http/httptest"
+	"regexp"
 	"testing"
 
 	httpm "goa.design/goa/v3/http/middleware"
@@ -26,26 +27,40 @@ func TestMiddleware(t *testing.T) {
 		spanID     = "testSpanID"
 		newTraceID = func() string { return traceID }
 		newID      = func() string { return spanID }
+		discard    = regexp.MustCompile("^/$")
 	)
 
 	cases := map[string]struct {
 		Rate                  int
 		TraceID, ParentSpanID string
+		Discard               *regexp.Regexp
 		// output
 		CtxTraceID, CtxSpanID, CtxParentID string
 	}{
-		"no-trace": {100, "", "", traceID, spanID, ""},
-		"trace":    {100, "trace", "", "trace", spanID, ""},
-		"parent":   {100, "trace", "parent", "trace", spanID, "parent"},
+		"no-trace":             {100, "", "", nil, traceID, spanID, ""},
+		"no-trace-discarded":   {100, "", "", discard, "", "", ""},
+		"trace":                {100, "trace", "", nil, "trace", spanID, ""},
+		"trace-not-discarded":  {100, "trace", "", discard, "trace", spanID, ""},
+		"parent":               {100, "trace", "parent", nil, "trace", spanID, "parent"},
+		"parent-not-discarded": {100, "trace", "parent", discard, "trace", spanID, "parent"},
 
-		"zero-rate-no-trace": {0, "", "", "", "", ""},
-		"zero-rate-trace":    {0, "trace", "", "trace", spanID, ""},
-		"zero-rate-parent":   {0, "trace", "parent", "trace", spanID, "parent"},
+		"zero-rate-no-trace":             {0, "", "", nil, "", "", ""},
+		"zero-rate-trace":                {0, "trace", "", nil, "trace", spanID, ""},
+		"zero-rate-trace-not-discarded":  {0, "trace", "", discard, "trace", spanID, ""},
+		"zero-rate-parent-not-discarded": {0, "trace", "parent", discard, "trace", spanID, "parent"},
 	}
 
 	for k, c := range cases {
+		traceOptions := []middleware.TraceOption{
+			httpm.SamplingPercent(c.Rate),
+			httpm.SpanIDFunc(newID),
+			httpm.TraceIDFunc(newTraceID),
+		}
+		if c.Discard != nil {
+			traceOptions = append(traceOptions, httpm.DiscardFromTrace(c.Discard))
+		}
 		var (
-			m       = httpm.Trace(httpm.SamplingPercent(c.Rate), httpm.SpanIDFunc(newID), httpm.TraceIDFunc(newTraceID))
+			m       = httpm.Trace(traceOptions...)
 			h       = new(testHandler)
 			headers = make(http.Header)
 		)
diff --git a/middleware/trace_test.go b/middleware/trace_test.go
index 8a87687c..242013e1 100644
--- a/middleware/trace_test.go
+++ b/middleware/trace_test.go
@@ -2,6 +2,7 @@ package middleware
 
 import (
 	"math"
+	"regexp"
 	"testing"
 )
 
@@ -92,4 +93,22 @@ func TestNewTraceOptions(t *testing.T) {
 			}()
 		}
 	}
+
+	// invalid discard
+	{
+		cases := map[string]struct{ Discard *regexp.Regexp }{
+			"nil": {nil},
+		}
+		for k, c := range cases {
+			func() {
+				defer func() {
+					r := recover()
+					if r != "discard cannot be nil" {
+						t.Errorf("DiscardFromTrace(%s): NewTraceOptions did *not* panic as expected: %v", k, r)
+					}
+				}()
+				NewTraceOptions(DiscardFromTrace(c.Discard))
+			}()
+		}
+	}
 }
diff --git a/pkg/validation_test.go b/pkg/validation_test.go
index 8970c94b..60fab7b1 100644
--- a/pkg/validation_test.go
+++ b/pkg/validation_test.go
@@ -19,7 +19,10 @@ func TestValidateFormat(t *testing.T) {
 		validUUID       = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
 		invalidUUID     = "96054a62-a9e45ed26688389b"
 		validEmail      = "raphael@goa.design"
-		invalidEmail    = "foo"
+
+		// Re-enable once CircleCI uses Go 1.13
+		// invalidEmail    = "foo"
+
 		validHostname   = "goa.design"
 		invalidHostname = "_hi_"
 		validIPv4       = "192.168.0.1"
@@ -45,14 +48,17 @@ func TestValidateFormat(t *testing.T) {
 		format   Format
 		expected error
 	}{
-		"valid date":         {"validDate", validDate, FormatDate, nil},
-		"invalid date":       {"invalidDate", invalidDate, FormatDate, InvalidFormatError("invalidDate", invalidDate, FormatDate, &time.ParseError{Layout: "2006-01-02", Value: invalidDate, LayoutElem: "-", ValueElem: invalidDate[4:]})},
-		"valid date-time":    {"validDateTime", validDateTime, FormatDateTime, nil},
-		"invalid date-time":  {"invalidDateTime", invalidDateTime, FormatDateTime, InvalidFormatError("invalidDateTime", invalidDateTime, FormatDateTime, &time.ParseError{Layout: time.RFC3339, Value: invalidDateTime, LayoutElem: "-", ValueElem: invalidDateTime[4:]})},
-		"valid uuid":         {"validUUID", validUUID, FormatUUID, nil},
-		"invalid uuid":       {"invalidUUID", invalidUUID, FormatUUID, InvalidFormatError("invalidUUID", invalidUUID, FormatUUID, fmt.Errorf("uuid: UUID string too short: %s", invalidUUID))},
-		"valid email":        {"validEmail", validEmail, FormatEmail, nil},
-		"invalid email":      {"invalidEmail", invalidEmail, FormatEmail, InvalidFormatError("invalidEmail", invalidEmail, FormatEmail, errors.New("mail: no angle-addr"))},
+		"valid date":        {"validDate", validDate, FormatDate, nil},
+		"invalid date":      {"invalidDate", invalidDate, FormatDate, InvalidFormatError("invalidDate", invalidDate, FormatDate, &time.ParseError{Layout: "2006-01-02", Value: invalidDate, LayoutElem: "-", ValueElem: invalidDate[4:]})},
+		"valid date-time":   {"validDateTime", validDateTime, FormatDateTime, nil},
+		"invalid date-time": {"invalidDateTime", invalidDateTime, FormatDateTime, InvalidFormatError("invalidDateTime", invalidDateTime, FormatDateTime, &time.ParseError{Layout: time.RFC3339, Value: invalidDateTime, LayoutElem: "-", ValueElem: invalidDateTime[4:]})},
+		"valid uuid":        {"validUUID", validUUID, FormatUUID, nil},
+		"invalid uuid":      {"invalidUUID", invalidUUID, FormatUUID, InvalidFormatError("invalidUUID", invalidUUID, FormatUUID, fmt.Errorf("uuid: UUID string too short: %s", invalidUUID))},
+		"valid email":       {"validEmail", validEmail, FormatEmail, nil},
+
+		// Re-enable once CircleCI uses Go 1.13
+		// "invalid email":      {"invalidEmail", invalidEmail, FormatEmail, InvalidFormatError("invalidEmail", invalidEmail, FormatEmail, errors.New("mail: missing '@' or angle-addr"))},
+
 		"valid hostname":     {"validHostname", validHostname, FormatHostname, nil},
 		"invalid hostname":   {"invalidHostname", invalidHostname, FormatHostname, InvalidFormatError("invalidHostname", invalidHostname, FormatHostname, fmt.Errorf("hostname value '%s' does not match %s", invalidHostname, `^[[:alnum:]][[:alnum:]\-]{0,61}[[:alnum:]]|[[:alpha:]]$`))},
 		"valid ipv4":         {"validIPv4", validIPv4, FormatIPv4, nil},