Skip to content

Commit

Permalink
Merge pull request #409 from onematchfox/metrics
Browse files Browse the repository at this point in the history
Update `otelfiber` metrics implementation
  • Loading branch information
ReneWerner87 authored Jan 16, 2023
2 parents 4d4a031 + f3bf30b commit 939d796
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 109 deletions.
2 changes: 1 addition & 1 deletion otelfiber/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ go get -u github.com/gofiber/contrib/otelfiber
### Signature

```
otelfiber.Middleware(service string, opts ...Option) fiber.Handler
otelfiber.Middleware(opts ...Option) fiber.Handler
```

### Usage
Expand Down
19 changes: 19 additions & 0 deletions otelfiber/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
type config struct {
TracerProvider oteltrace.TracerProvider
MeterProvider otelmetric.MeterProvider
Port *int
Propagators propagation.TextMapPropagator
ServerName *string
SpanNameFormatter func(*fiber.Ctx) string
}

Expand Down Expand Up @@ -58,3 +60,20 @@ func WithSpanNameFormatter(f func(ctx *fiber.Ctx) string) Option {
cfg.SpanNameFormatter = f
})
}

// WithServerName specifies the value to use when setting the `http.server_name`
// attribute on metrics/spans.
func WithServerName(serverName string) Option {
return optionFunc(func(cfg *config) {
cfg.ServerName = &serverName
})
}

// WithPort specifies the value to use when setting the `net.host.port`
// attribute on metrics/spans. Attribute is "Conditionally Required: If not
// default (`80` for `http`, `443` for `https`).
func WithPort(port int) Option {
return optionFunc(func(cfg *config) {
cfg.Port = &port
})
}
8 changes: 5 additions & 3 deletions otelfiber/example/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ package main
import (
"context"
"errors"
"go.opentelemetry.io/otel/sdk/resource"
"log"

"go.opentelemetry.io/otel/sdk/resource"

"github.com/gofiber/fiber/v2"

"github.com/gofiber/contrib/otelfiber"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
stdout "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"

//"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
Expand All @@ -32,11 +34,11 @@ func main() {
app := fiber.New()

// customise span name
//app.Use(otelfiber.Middleware("my-server", otelfiber.WithSpanNameFormatter(func(ctx *fiber.Ctx) string {
//app.Use(otelfiber.Middleware(otelfiber.WithSpanNameFormatter(func(ctx *fiber.Ctx) string {
// return fmt.Sprintf("%s - %s", ctx.Method(), ctx.Route().Path)
//})))

app.Use(otelfiber.Middleware("my-server"))
app.Use(otelfiber.Middleware())

app.Get("/error", func(ctx *fiber.Ctx) error {
return errors.New("abc")
Expand Down
125 changes: 48 additions & 77 deletions otelfiber/fiber.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,26 @@ package otelfiber

import (
"context"
"encoding/base64"
"net/http"
"strings"
"time"

"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
otelcontrib "go.opentelemetry.io/contrib"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/metric/instrument"
"go.opentelemetry.io/otel/metric/unit"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
oteltrace "go.opentelemetry.io/otel/trace"
)

const (
tracerKey = "gofiber-contrib-tracer-fiber"
instrumentationName = "github.com/gofiber/contrib/otelfiber"
defaultServiceName = "fiber-server"

metricNameHttpServerDuration = "http.server.duration"
metricNameHttpServerRequestSize = "http.server.request.size"
Expand All @@ -32,28 +30,28 @@ const (
)

// Middleware returns fiber handler which will trace incoming requests.
func Middleware(service string, opts ...Option) fiber.Handler {
if service == "" {
service = defaultServiceName
}
func Middleware(opts ...Option) fiber.Handler {
cfg := config{}
for _, opt := range opts {
opt.apply(&cfg)
}

if cfg.TracerProvider == nil {
cfg.TracerProvider = otel.GetTracerProvider()
}
tracer := cfg.TracerProvider.Tracer(
instrumentationName,
oteltrace.WithInstrumentationVersion(otelcontrib.SemVersion()),
)

if cfg.MeterProvider == nil {
cfg.MeterProvider = global.MeterProvider()
}
meter := cfg.MeterProvider.Meter(
instrumentationName,
metric.WithInstrumentationVersion(otelcontrib.SemVersion()),
)

httpServerDuration, err := meter.SyncFloat64().Histogram(metricNameHttpServerDuration, instrument.WithUnit(unit.Milliseconds), instrument.WithDescription("measures the duration inbound HTTP requests"))
if err != nil {
otel.Handle(err)
Expand All @@ -70,6 +68,7 @@ func Middleware(service string, opts ...Option) fiber.Handler {
if err != nil {
otel.Handle(err)
}

if cfg.Propagators == nil {
cfg.Propagators = otel.GetTextMapPropagator()
}
Expand All @@ -82,45 +81,25 @@ func Middleware(service string, opts ...Option) fiber.Handler {
savedCtx, cancel := context.WithCancel(c.UserContext())

start := time.Now()
metricAttrs := httpServerMetricAttributesFromRequest(c, service)
httpServerActiveRequests.Add(savedCtx, 1, metricAttrs...)
defer func() {
httpServerDuration.Record(savedCtx, float64(time.Since(start).Microseconds())/1000, metricAttrs...)
httpServerRequestSize.Record(savedCtx, int64(len(c.Request().Body())), metricAttrs...)
httpServerResponseSize.Record(savedCtx, int64(len(c.Response().Body())), metricAttrs...)
httpServerActiveRequests.Add(savedCtx, -1, metricAttrs...)
c.SetUserContext(savedCtx)
cancel()
}()

requestMetricsAttrs := httpServerMetricAttributesFromRequest(c, cfg)
httpServerActiveRequests.Add(savedCtx, 1, requestMetricsAttrs...)

responseMetricAttrs := make([]attribute.KeyValue, len(requestMetricsAttrs))
copy(responseMetricAttrs, requestMetricsAttrs)

reqHeader := make(http.Header)
c.Request().Header.VisitAll(func(k, v []byte) {
reqHeader.Add(string(k), string(v))
})

ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(reqHeader))

opts := []oteltrace.SpanStartOption{
oteltrace.WithAttributes(
// utils.CopyString: we need to copy the string as fasthttp strings are by default
// mutable so it will be unsafe to use in this middleware as it might be used after
// the handler returns.
semconv.HTTPServerNameKey.String(service),
semconv.HTTPMethodKey.String(utils.CopyString(c.Method())),
semconv.HTTPTargetKey.String(string(utils.CopyBytes(c.Request().RequestURI()))),
semconv.HTTPURLKey.String(utils.CopyString(c.OriginalURL())),
semconv.NetHostIPKey.String(utils.CopyString(c.IP())),
semconv.NetHostNameKey.String(utils.CopyString(c.Hostname())),
semconv.HTTPUserAgentKey.String(string(utils.CopyBytes(c.Request().Header.UserAgent()))),
semconv.HTTPRequestContentLengthKey.Int(c.Request().Header.ContentLength()),
semconv.HTTPSchemeKey.String(utils.CopyString(c.Protocol())),
semconv.NetTransportTCP),
oteltrace.WithAttributes(httpServerTraceAttributesFromRequest(c, cfg)...),
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
}
if username, ok := hasBasicAuth(c.Get(fiber.HeaderAuthorization)); ok {
opts = append(opts, oteltrace.WithAttributes(semconv.EnduserIDKey.String(utils.CopyString(username))))
}
if len(c.IPs()) > 0 {
opts = append(opts, oteltrace.WithAttributes(semconv.HTTPClientIPKey.String(utils.CopyString(c.IPs()[0]))))
}

// temporary set to c.Path() first
// update with c.Route().Path after c.Next() is called
// to get pathRaw
Expand All @@ -132,22 +111,44 @@ func Middleware(service string, opts ...Option) fiber.Handler {
c.SetUserContext(ctx)

// serve the request to the next middleware
err := c.Next()

span.SetName(cfg.SpanNameFormatter(c))
// no need to copy c.Route().Path: route strings should be immutable across app lifecycle
span.SetAttributes(semconv.HTTPRouteKey.String(c.Route().Path))

if err != nil {
if err := c.Next(); err != nil {
span.RecordError(err)
// invokes the registered HTTP error handler
// to get the correct response status code
_ = c.App().Config().ErrorHandler(c, err)
}

attrs := semconv.HTTPAttributesFromHTTPStatusCode(c.Response().StatusCode())
// extract common attributes from response
responseAttrs := append(
semconv.HTTPAttributesFromHTTPStatusCode(c.Response().StatusCode()),
semconv.HTTPRouteKey.String(c.Route().Path), // no need to copy c.Route().Path: route strings should be immutable across app lifecycle
)

requestSize := int64(len(c.Request().Body()))
responseSize := int64(len(c.Response().Body()))

defer func() {
responseMetricAttrs = append(
responseMetricAttrs,
responseAttrs...)

httpServerActiveRequests.Add(savedCtx, -1, requestMetricsAttrs...)
httpServerDuration.Record(savedCtx, float64(time.Since(start).Microseconds())/1000, responseMetricAttrs...)
httpServerRequestSize.Record(savedCtx, requestSize, responseMetricAttrs...)
httpServerResponseSize.Record(savedCtx, responseSize, responseMetricAttrs...)

c.SetUserContext(savedCtx)
cancel()
}()

span.SetAttributes(
append(
responseAttrs,
semconv.HTTPResponseContentLengthKey.Int64(responseSize),
)...)
span.SetName(cfg.SpanNameFormatter(c))

spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(c.Response().StatusCode(), oteltrace.SpanKindServer)
span.SetAttributes(attrs...)
span.SetStatus(spanStatus, spanMessage)

return nil
Expand All @@ -159,33 +160,3 @@ func Middleware(service string, opts ...Option) fiber.Handler {
func defaultSpanNameFormatter(ctx *fiber.Ctx) string {
return ctx.Route().Path
}

func hasBasicAuth(auth string) (string, bool) {
if auth == "" {
return "", false
}

// Check if the Authorization header is Basic
if !strings.HasPrefix(auth, "Basic ") {
return "", false
}

// Decode the header contents
raw, err := base64.StdEncoding.DecodeString(auth[6:])
if err != nil {
return "", false
}

// Get the credentials
creds := utils.UnsafeString(raw)

// Check if the credentials are in the correct form
// which is "username:password".
index := strings.Index(creds, ":")
if index == -1 {
return "", false
}

// Get the username
return creds[:index], true
}
Loading

0 comments on commit 939d796

Please sign in to comment.