1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-12-24 00:01:31 +02:00

update tracing, fixed docker-compose and removed vendor dir

This commit is contained in:
Lee Brown
2019-05-23 19:40:29 -05:00
parent c77dd8f5f3
commit c19f46e07f
264 changed files with 391 additions and 45102 deletions

View File

@@ -5,10 +5,10 @@ import (
"net/http"
"strings"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/pkg/errors"
"go.opencensus.io/trace"
)
// ErrForbidden is returned when an authenticated user does not have a
@@ -26,8 +26,8 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "internal.mid.Authenticate")
defer span.End()
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Authenticate")
defer span.Finish()
authHdr := r.Header.Get("Authorization")
if authHdr == "" {
@@ -65,8 +65,8 @@ func HasRole(roles ...string) web.Middleware {
f := func(after web.Handler) web.Handler {
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "internal.mid.HasRole")
defer span.End()
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.HasRole")
defer span.Finish()
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {

View File

@@ -5,8 +5,8 @@ import (
"log"
"net/http"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"go.opencensus.io/trace"
)
// Errors handles errors coming out of the call chain. It detects normal
@@ -19,20 +19,13 @@ func Errors(log *log.Logger) web.Middleware {
// Create the handler that will be attached in the middleware chain.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "internal.mid.Errors")
defer span.End()
// If the context is missing this value, request the service
// to be shutdown gracefully.
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Errors")
defer span.Finish()
if err := before(ctx, w, r, params); err != nil {
// Log the error.
log.Printf("%s : ERROR : %+v", v.TraceID, err)
log.Printf("%d : ERROR : %+v", span.Context().TraceID(), err)
// Respond to the error.
if err := web.RespondError(ctx, w, err); err != nil {

View File

@@ -6,8 +6,8 @@ import (
"net/http"
"time"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"go.opencensus.io/trace"
)
// Logger writes some information about the request to the logs in the
@@ -19,8 +19,8 @@ func Logger(log *log.Logger) web.Middleware {
// Create the handler that will be attached in the middleware chain.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "internal.mid.Logger")
defer span.End()
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Logger")
defer span.Finish()
// If the context is missing this value, request the service
// to be shutdown gracefully.
@@ -31,8 +31,8 @@ func Logger(log *log.Logger) web.Middleware {
err := before(ctx, w, r, params)
log.Printf("%s : (%d) : %s %s -> %s (%s)\n",
v.TraceID,
log.Printf("%d : (%d) : %s %s -> %s (%s)\n",
span.Context().TraceID(),
v.StatusCode,
r.Method, r.URL.Path,
r.RemoteAddr, time.Since(v.Now),

View File

@@ -6,8 +6,8 @@ import (
"net/http"
"runtime"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"go.opencensus.io/trace"
)
// m contains the global program counters for the application.
@@ -29,8 +29,8 @@ func Metrics() web.Middleware {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctx, span := trace.StartSpan(ctx, "internal.mid.Metrics")
defer span.End()
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Metrics")
defer span.Finish()
err := before(ctx, w, r, params)

View File

@@ -5,9 +5,9 @@ import (
"net/http"
"runtime/debug"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/pkg/errors"
"go.opencensus.io/trace"
)
// Panics recovers from panics and converts the panic to an error so it is
@@ -19,8 +19,8 @@ func Panics() web.Middleware {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) (err error) {
ctx, span := trace.StartSpan(ctx, "internal.mid.Panics")
defer span.End()
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Panics")
defer span.Finish()
// Defer a function to recover from a panic and set the err return variable
// after the fact. Using the errors package will generate a stack trace.

View File

@@ -0,0 +1,64 @@
package mid
import (
"context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"net/http"
)
// Trace adds the base tracing info for requests
func Trace() web.Middleware {
// This is the actual middleware function to be executed.
f := func(before web.Handler) web.Handler {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// Span options with request info
opts := []ddtrace.StartSpanOption{
tracer.SpanType(ext.SpanTypeWeb),
tracer.ResourceName(r.URL.Path),
tracer.Tag(ext.HTTPMethod, r.Method),
tracer.Tag(ext.HTTPURL, r.RequestURI),
}
// Continue server side request tracing from previous request.
if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil {
opts = append(opts, tracer.ChildOf(spanctx))
}
// Start the span for tracking
span, ctx := tracer.StartSpanFromContext(ctx, "http.request", opts...)
defer span.Finish()
// If the context is missing this value, request the service
// to be shutdown gracefully.
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
// Execute the request handler
err := before(ctx, w, r, params)
// Set the span status code for the trace
span.SetTag(ext.HTTPCode, v.StatusCode)
// If there was an error, append it to the span
if err != nil {
span.SetTag(ext.Error, fmt.Sprintf("%+v", err))
}
// Return the error so it can be handled further up the chain.
return err
}
return h
}
return f
}

View File

@@ -1,194 +0,0 @@
package trace
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"sync"
"time"
"go.opencensus.io/trace"
)
// Error variables for factory validation.
var (
ErrLoggerNotProvided = errors.New("logger not provided")
ErrHostNotProvided = errors.New("host not provided")
)
// Log provides support for logging inside this package.
// Unfortunately, the opentrace API calls into the ExportSpan
// function directly with no means to pass user defined arguments.
type Log func(format string, v ...interface{})
// Exporter provides support to batch spans and send them
// to the sidecar for processing.
type Exporter struct {
log Log // Handler function for logging.
host string // IP:port of the sidecare consuming the trace data.
batchSize int // Size of the batch of spans before sending.
sendInterval time.Duration // Time to send a batch if batch size is not met.
sendTimeout time.Duration // Time to wait for the sidecar to respond on send.
client http.Client // Provides APIs for performing the http send.
batch []*trace.SpanData // Maintains the batch of span data to be sent.
mu sync.Mutex // Provide synchronization to access the batch safely.
timer *time.Timer // Signals when the sendInterval is met.
}
// NewExporter creates an exporter for use.
func NewExporter(log Log, host string, batchSize int, sendInterval, sendTimeout time.Duration) (*Exporter, error) {
if log == nil {
return nil, ErrLoggerNotProvided
}
if host == "" {
return nil, ErrHostNotProvided
}
tr := http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 2,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
e := Exporter{
log: log,
host: host,
batchSize: batchSize,
sendInterval: sendInterval,
sendTimeout: sendTimeout,
client: http.Client{
Transport: &tr,
},
batch: make([]*trace.SpanData, 0, batchSize),
timer: time.NewTimer(sendInterval),
}
return &e, nil
}
// Close sends the remaining spans that have not been sent yet.
func (e *Exporter) Close() (int, error) {
var sendBatch []*trace.SpanData
e.mu.Lock()
{
sendBatch = e.batch
}
e.mu.Unlock()
err := e.send(sendBatch)
if err != nil {
return len(sendBatch), err
}
return len(sendBatch), nil
}
// ExportSpan is called by opentracing when spans are created. It implements
// the Exporter interface.
func (e *Exporter) ExportSpan(span *trace.SpanData) {
sendBatch := e.saveBatch(span)
if sendBatch != nil {
go func() {
e.log("trace : Exporter : ExportSpan : Sending Batch[%d]", len(sendBatch))
if err := e.send(sendBatch); err != nil {
e.log("trace : Exporter : ExportSpan : ERROR : %v", err)
}
}()
}
}
// Saves the span data to the batch. If the batch should be sent,
// returns a batch to send.
func (e *Exporter) saveBatch(span *trace.SpanData) []*trace.SpanData {
var sendBatch []*trace.SpanData
e.mu.Lock()
{
// We want to append this new span to the collection.
e.batch = append(e.batch, span)
// Do we need to send the current batch?
switch {
case len(e.batch) == e.batchSize:
// We hit the batch size. Now save the current
// batch for sending and start a new batch.
sendBatch = e.batch
e.batch = make([]*trace.SpanData, 0, e.batchSize)
e.timer.Reset(e.sendInterval)
default:
// We did not hit the batch size but maybe send what
// we have based on time.
select {
case <-e.timer.C:
// The time has expired so save the current
// batch for sending and start a new batch.
sendBatch = e.batch
e.batch = make([]*trace.SpanData, 0, e.batchSize)
e.timer.Reset(e.sendInterval)
// It's not time yet, just move on.
default:
}
}
}
e.mu.Unlock()
return sendBatch
}
// send uses HTTP to send the data to the tracing sidecare for processing.
func (e *Exporter) send(sendBatch []*trace.SpanData) error {
data, err := json.Marshal(sendBatch)
if err != nil {
return err
}
req, err := http.NewRequest("POST", e.host, bytes.NewBuffer(data))
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(req.Context(), e.sendTimeout)
defer cancel()
req = req.WithContext(ctx)
ch := make(chan error)
go func() {
resp, err := e.client.Do(req)
if err != nil {
ch <- err
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
ch <- fmt.Errorf("error on call : status[%s]", resp.Status)
return
}
ch <- fmt.Errorf("error on call : status[%s] : %s", resp.Status, string(data))
return
}
ch <- nil
}()
return <-ch
}

View File

@@ -1,278 +0,0 @@
package trace
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"go.opencensus.io/trace"
)
// Success and failure markers.
const (
success = "\u2713"
failed = "\u2717"
)
// inputSpans represents spans of data for the tests.
var inputSpans = []*trace.SpanData{
{Name: "span1"},
{Name: "span2"},
{Name: "span3"},
}
// inputSpansJSON represents a JSON representation of the span data.
var inputSpansJSON = `[{"TraceID":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"SpanID":[0,0,0,0,0,0,0,0],"TraceOptions":0,"ParentSpanID":[0,0,0,0,0,0,0,0],"SpanKind":0,"Name":"span1","StartTime":"0001-01-01T00:00:00Z","EndTime":"0001-01-01T00:00:00Z","Attributes":null,"Annotations":null,"MessageEvents":null,"Code":0,"Message":"","Links":null,"HasRemoteParent":false},{"TraceID":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"SpanID":[0,0,0,0,0,0,0,0],"TraceOptions":0,"ParentSpanID":[0,0,0,0,0,0,0,0],"SpanKind":0,"Name":"span2","StartTime":"0001-01-01T00:00:00Z","EndTime":"0001-01-01T00:00:00Z","Attributes":null,"Annotations":null,"MessageEvents":null,"Code":0,"Message":"","Links":null,"HasRemoteParent":false},{"TraceID":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"SpanID":[0,0,0,0,0,0,0,0],"TraceOptions":0,"ParentSpanID":[0,0,0,0,0,0,0,0],"SpanKind":0,"Name":"span3","StartTime":"0001-01-01T00:00:00Z","EndTime":"0001-01-01T00:00:00Z","Attributes":null,"Annotations":null,"MessageEvents":null,"Code":0,"Message":"","Links":null,"HasRemoteParent":false}]`
// =============================================================================
// logger is required to create an Exporter.
var logger = func(format string, v ...interface{}) {
log.Printf(format, v)
}
// MakeExporter abstracts the error handling aspects of creating an Exporter.
func makeExporter(host string, batchSize int, sendInterval, sendTimeout time.Duration) *Exporter {
exporter, err := NewExporter(logger, host, batchSize, sendInterval, sendTimeout)
if err != nil {
log.Fatalln("Unable to create exporter, ", err)
}
return exporter
}
// =============================================================================
var saveTests = []struct {
name string
e *Exporter
input []*trace.SpanData
output []*trace.SpanData
lastSaveDelay time.Duration // The delay before the last save. For testing intervals.
isInputMatchBatch bool // If the input should match the internal exporter collection after the last save.
isSendBatch bool // If the last save should return nil or batch data.
}{
{"NoSend", makeExporter("test", 10, time.Minute, time.Second), inputSpans, nil, time.Nanosecond, true, false},
{"SendOnBatchSize", makeExporter("test", 3, time.Minute, time.Second), inputSpans, inputSpans, time.Nanosecond, false, true},
{"SendOnTime", makeExporter("test", 4, time.Millisecond, time.Second), inputSpans, inputSpans, 2 * time.Millisecond, false, true},
}
// TestSave validates the save batch functionality is working.
func TestSave(t *testing.T) {
t.Log("Given the need to validate saving span data to a batch.")
{
for i, tt := range saveTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
// Save the input of span data.
l := len(tt.input) - 1
var batch []*trace.SpanData
for i, span := range tt.input {
// If this is the last save, take the configured delay.
// We might be testing invertal based batching.
if l == i {
time.Sleep(tt.lastSaveDelay)
}
batch = tt.e.saveBatch(span)
}
// Compare the internal collection with what we saved.
if tt.isInputMatchBatch {
if len(tt.e.batch) != len(tt.input) {
t.Log("\t\tGot :", len(tt.e.batch))
t.Log("\t\tWant:", len(tt.input))
t.Errorf("\t%s\tShould have the same number of spans as input.", failed)
} else {
t.Logf("\t%s\tShould have the same number of spans as input.", success)
}
} else {
if len(tt.e.batch) != 0 {
t.Log("\t\tGot :", len(tt.e.batch))
t.Log("\t\tWant:", 0)
t.Errorf("\t%s\tShould have zero spans.", failed)
} else {
t.Logf("\t%s\tShould have zero spans.", success)
}
}
// Validate the return provided or didn't provide a batch to send.
if !tt.isSendBatch && batch != nil {
t.Errorf("\t%s\tShould not have a batch to send.", failed)
} else if !tt.isSendBatch {
t.Logf("\t%s\tShould not have a batch to send.", success)
}
if tt.isSendBatch && batch == nil {
t.Errorf("\t%s\tShould have a batch to send.", failed)
} else if tt.isSendBatch {
t.Logf("\t%s\tShould have a batch to send.", success)
}
// Compare the batch to send.
if !reflect.DeepEqual(tt.output, batch) {
t.Log("\t\tGot :", batch)
t.Log("\t\tWant:", tt.output)
t.Errorf("\t%s\tShould have an expected match of the batch to send.", failed)
} else {
t.Logf("\t%s\tShould have an expected match of the batch to send.", success)
}
}
}
}
}
// =============================================================================
var sendTests = []struct {
name string
e *Exporter
input []*trace.SpanData
pass bool
}{
{"success", makeExporter("test", 3, time.Minute, time.Hour), inputSpans, true},
{"failure", makeExporter("test", 3, time.Minute, time.Hour), inputSpans[:2], false},
{"timeout", makeExporter("test", 3, time.Minute, time.Nanosecond), inputSpans, false},
}
// mockServer returns a pointer to a server to handle the mock get call.
func mockServer() *httptest.Server {
f := func(w http.ResponseWriter, r *http.Request) {
d, _ := ioutil.ReadAll(r.Body)
data := string(d)
if data != inputSpansJSON {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, data)
return
}
w.WriteHeader(http.StatusNoContent)
}
return httptest.NewServer(http.HandlerFunc(f))
}
// TestSend validates spans can be sent to the sidecar.
func TestSend(t *testing.T) {
s := mockServer()
defer s.Close()
t.Log("Given the need to validate sending span data to the sidecar.")
{
for i, tt := range sendTests {
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
{
// Set the URL for the call.
tt.e.host = s.URL
// Send the span data.
err := tt.e.send(tt.input)
if tt.pass {
if err != nil {
t.Errorf("\t%s\tShould be able to send the batch successfully: %v", failed, err)
} else {
t.Logf("\t%s\tShould be able to send the batch successfully.", success)
}
} else {
if err == nil {
t.Errorf("\t%s\tShould not be able to send the batch successfully : %v", failed, err)
} else {
t.Logf("\t%s\tShould not be able to send the batch successfully.", success)
}
}
}
}
}
}
// TestClose validates the flushing of the final batched spans.
func TestClose(t *testing.T) {
s := mockServer()
defer s.Close()
t.Log("Given the need to validate flushing the remaining batched spans.")
{
t.Logf("\tTest: %d\tWhen running test: %s", 0, "FlushWithData")
{
e, err := NewExporter(logger, "test", 10, time.Minute, time.Hour)
if err != nil {
t.Fatalf("\t%s\tShould be able to create an Exporter : %v", failed, err)
}
t.Logf("\t%s\tShould be able to create an Exporter.", success)
// Set the URL for the call.
e.host = s.URL
// Save the input of span data.
for _, span := range inputSpans {
e.saveBatch(span)
}
// Close the Exporter and we should get those spans sent.
sent, err := e.Close()
if err != nil {
t.Fatalf("\t%s\tShould be able to flush the Exporter : %v", failed, err)
}
t.Logf("\t%s\tShould be able to flush the Exporter.", success)
if sent != len(inputSpans) {
t.Log("\t\tGot :", sent)
t.Log("\t\tWant:", len(inputSpans))
t.Fatalf("\t%s\tShould have flushed the expected number of spans.", failed)
}
t.Logf("\t%s\tShould have flushed the expected number of spans.", success)
}
t.Logf("\tTest: %d\tWhen running test: %s", 0, "FlushWithError")
{
e, err := NewExporter(logger, "test", 10, time.Minute, time.Hour)
if err != nil {
t.Fatalf("\t%s\tShould be able to create an Exporter : %v", failed, err)
}
t.Logf("\t%s\tShould be able to create an Exporter.", success)
// Set the URL for the call.
e.host = s.URL
// Save the input of span data.
for _, span := range inputSpans[:2] {
e.saveBatch(span)
}
// Close the Exporter and we should get those spans sent.
if _, err := e.Close(); err == nil {
t.Fatalf("\t%s\tShould not be able to flush the Exporter.", failed)
}
t.Logf("\t%s\tShould not be able to flush the Exporter.", success)
}
}
}
// =============================================================================
// TestExporterFailure validates misuse cases are covered.
func TestExporterFailure(t *testing.T) {
t.Log("Given the need to validate Exporter initializes properly.")
{
t.Logf("\tTest: %d\tWhen not passing a proper logger.", 0)
{
_, err := NewExporter(nil, "test", 10, time.Minute, time.Hour)
if err == nil {
t.Errorf("\t%s\tShould not be able to create an Exporter.", failed)
} else {
t.Logf("\t%s\tShould not be able to create an Exporter.", success)
}
}
t.Logf("\tTest: %d\tWhen not passing a proper host.", 1)
{
_, err := NewExporter(logger, "", 10, time.Minute, time.Hour)
if err == nil {
t.Errorf("\t%s\tShould not be able to create an Exporter.", failed)
} else {
t.Logf("\t%s\tShould not be able to create an Exporter.", success)
}
}
}
}

View File

@@ -9,9 +9,6 @@ import (
"time"
"github.com/dimfeld/httptreemux"
"go.opencensus.io/plugin/ochttp"
"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
"go.opencensus.io/trace"
)
// ctxKey represents the type of value for the context key.
@@ -22,7 +19,6 @@ const KeyValues ctxKey = 1
// Values represent state for each request.
type Values struct {
TraceID string
Now time.Time
StatusCode int
}
@@ -36,7 +32,6 @@ type Handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, p
// data/logic on this App struct
type App struct {
*httptreemux.TreeMux
och *ochttp.Handler
shutdown chan os.Signal
log *log.Logger
mw []Middleware
@@ -51,17 +46,6 @@ func NewApp(shutdown chan os.Signal, log *log.Logger, mw ...Middleware) *App {
mw: mw,
}
// Create an OpenCensus HTTP Handler which wraps the router. This will start
// the initial span and annotate it with information about the request/response.
//
// This is configured to use the W3C TraceContext standard to set the remote
// parent if an client request includes the appropriate headers.
// https://w3c.github.io/trace-context/
app.och = &ochttp.Handler{
Handler: app.TreeMux,
Propagation: &tracecontext.HTTPFormat{},
}
return &app
}
@@ -84,16 +68,12 @@ func (a *App) Handle(verb, path string, handler Handler, mw ...Middleware) {
// The function to execute for each request.
h := func(w http.ResponseWriter, r *http.Request, params map[string]string) {
ctx, span := trace.StartSpan(r.Context(), "internal.platform.web")
defer span.End()
// Set the context with the required values to
// process the request.
v := Values{
TraceID: span.SpanContext().TraceID.String(),
Now: time.Now(),
}
ctx = context.WithValue(ctx, KeyValues, &v)
ctx := context.WithValue(r.Context(), KeyValues, &v)
// Call the wrapped handler functions.
if err := handler(ctx, w, r, params); err != nil {
@@ -106,10 +86,3 @@ func (a *App) Handle(verb, path string, handler Handler, mw ...Middleware) {
// Add this handler for the specified verb and route.
a.TreeMux.Handle(verb, path, h)
}
// ServeHTTP implements the http.Handler interface. It overrides the ServeHTTP
// of the embedded TreeMux by using the ochttp.Handler instead. That Handler
// wraps the TreeMux handler so the routes are served.
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
a.och.ServeHTTP(w, r)
}