module/apmfiber/middleware.go (107 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you 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 apmfiber // import "go.elastic.co/apm/module/apmfiber/v2"
import (
"errors"
"net/http"
"github.com/gofiber/fiber/v2"
"go.elastic.co/apm/module/apmfasthttp/v2"
"go.elastic.co/apm/module/apmhttp/v2"
"go.elastic.co/apm/v2"
)
// Middleware returns a new Fiber middleware handler for tracing
// requests and reporting errors.
//
// This middleware will recover and report panics, so it can
// be used instead of default recover middleware.
//
// By default, the middleware will use apm.DefaultTracer().
// Use WithTracer to specify an alternative tracer.
// Use WithPanicPropagation to disable panic recover.
func Middleware(o ...Option) fiber.Handler {
m := &middleware{
tracer: apm.DefaultTracer(),
requestIgnorer: apmfasthttp.NewDynamicServerRequestIgnorer(apm.DefaultTracer()),
panicPropagation: false,
}
for _, o := range o {
o(m)
}
return m.handle
}
type middleware struct {
tracer *apm.Tracer
requestIgnorer apmfasthttp.RequestIgnorerFunc
panicPropagation bool
}
func (m *middleware) handle(c *fiber.Ctx) (result error) {
reqCtx := c.Context()
if !m.tracer.Recording() || m.requestIgnorer(reqCtx) {
return c.Next()
}
name := string(reqCtx.Method()) + " " + c.Path()
tx, body, err := apmfasthttp.StartTransactionWithBody(reqCtx, m.tracer, name)
if err != nil {
reqCtx.Error(err.Error(), http.StatusInternalServerError)
return err
}
defer func() {
resp := c.Response()
route := c.Route()
var fiberErr *fiber.Error
if route.Path == "/" && errors.As(result, &fiberErr) && fiberErr.Code == http.StatusNotFound {
tx.Name = string(reqCtx.Method()) + " unknown route"
} else {
// Workaround for set tx.Name as template path, not absolute
tx.Name = string(reqCtx.Method()) + " " + route.Path
}
if v := recover(); v != nil {
if m.panicPropagation {
defer panic(v)
}
e := m.tracer.Recovered(v)
e.SetTransaction(tx)
setContext(&e.Context, resp)
e.Send()
c.Status(http.StatusInternalServerError)
}
statusCode := resp.StatusCode()
tx.Result = apmhttp.StatusCodeResult(statusCode)
if tx.Sampled() {
setContext(&tx.Context, resp)
}
body.Discard()
}()
result = c.Next()
if result != nil {
resp := c.Response()
e := m.tracer.NewError(result)
e.Handled = true
e.SetTransaction(tx)
setContext(&e.Context, resp)
e.Send()
}
return result
}
func setContext(ctx *apm.Context, resp *fiber.Response) {
ctx.SetFramework("fiber", fiber.Version)
ctx.SetHTTPStatusCode(resp.StatusCode())
headers := make(http.Header)
resp.Header.VisitAll(func(k, v []byte) {
sk := string(k)
sv := string(v)
headers.Set(sk, sv)
})
ctx.SetHTTPResponseHeaders(headers)
}
// Option sets options for tracing.
type Option func(*middleware)
// WithPanicPropagation returns an Option which enable panic propagation.
// Any panic will be recovered and recorded as an error in a transaction, then
// panic will be caused again.
func WithPanicPropagation() Option {
return func(o *middleware) {
o.panicPropagation = true
}
}
// WithTracer returns an Option which sets t as the tracer
// to use for tracing server requests. If t is nil, using default tracer.
func WithTracer(t *apm.Tracer) Option {
if t == nil {
return noopOption
}
return func(o *middleware) {
o.tracer = t
}
}
// WithRequestIgnorer returns an Option which sets fn as the
// function to use to determine whether or not a request should
// be ignored. If fn is nil, using default RequestIgnorerFunc.
func WithRequestIgnorer(fn apmfasthttp.RequestIgnorerFunc) Option {
if fn == nil {
return noopOption
}
return func(o *middleware) {
o.requestIgnorer = fn
}
}
func noopOption(_ *middleware) {}