[feature] Initial Prometheus metrics implementation (#2334)

* feat: Initial OTEL metrics

* docs: add metrics documentation

* fix: metrics endpoint conditional check

* feat: metrics endpoint basic auth

* fix: make metrics-auth-enabled default false

* fix: go fmt helpers.gen.go

* fix: add metric-related env vars to envparsing.sh

* fix: metrics docs

* fix: metrics related stuff in envparsing.sh

* fix: metrics docs

* chore: metrics docs wording

* fix: metrics stuff in envparsing?

* bump otel versions

---------

Co-authored-by: Tsuribori <user@acertaindebian>
Co-authored-by: Tsuribori <none@example.org>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
This commit is contained in:
Tsuribori 2023-11-20 17:43:55 +02:00 committed by GitHub
commit 1ba3e14b36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
251 changed files with 48389 additions and 22 deletions

View file

@ -0,0 +1,45 @@
package otelginmetrics
import (
"net/http"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
)
type config struct {
recordInFlight bool
recordSize bool
recordDuration bool
groupedStatus bool
recorder Recorder
attributes func(serverName, route string, request *http.Request) []attribute.KeyValue
shouldRecord func(serverName, route string, request *http.Request) bool
}
func defaultConfig() *config {
return &config{
recordInFlight: true,
recordDuration: true,
recordSize: true,
groupedStatus: true,
attributes: DefaultAttributes,
shouldRecord: func(_, _ string, _ *http.Request) bool {
return true
},
}
}
var DefaultAttributes = func(serverName, route string, request *http.Request) []attribute.KeyValue {
attrs := []attribute.KeyValue{
semconv.HTTPMethodKey.String(request.Method),
}
if serverName != "" {
attrs = append(attrs, semconv.HTTPServerNameKey.String(serverName))
}
if route != "" {
attrs = append(attrs, semconv.HTTPRouteKey.String(route))
}
return attrs
}

View file

@ -0,0 +1,94 @@
package otelginmetrics
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
)
// Middleware returns middleware that will trace incoming requests.
// The service parameter should describe the name of the (virtual)
// server handling the request.
func Middleware(service string, options ...Option) gin.HandlerFunc {
cfg := defaultConfig()
for _, option := range options {
option.apply(cfg)
}
recorder := cfg.recorder
if recorder == nil {
recorder = GetRecorder("")
}
return func(ginCtx *gin.Context) {
ctx := ginCtx.Request.Context()
route := ginCtx.FullPath()
if len(route) <= 0 {
route = "nonconfigured"
}
if !cfg.shouldRecord(service, route, ginCtx.Request) {
ginCtx.Next()
return
}
start := time.Now()
reqAttributes := cfg.attributes(service, route, ginCtx.Request)
if cfg.recordInFlight {
recorder.AddInflightRequests(ctx, 1, reqAttributes)
defer recorder.AddInflightRequests(ctx, -1, reqAttributes)
}
defer func() {
resAttributes := append(reqAttributes[0:0], reqAttributes...)
if cfg.groupedStatus {
code := int(ginCtx.Writer.Status()/100) * 100
resAttributes = append(resAttributes, semconv.HTTPStatusCodeKey.Int(code))
} else {
resAttributes = append(resAttributes, semconv.HTTPAttributesFromHTTPStatusCode(ginCtx.Writer.Status())...)
}
recorder.AddRequests(ctx, 1, resAttributes)
if cfg.recordSize {
requestSize := computeApproximateRequestSize(ginCtx.Request)
recorder.ObserveHTTPRequestSize(ctx, requestSize, resAttributes)
recorder.ObserveHTTPResponseSize(ctx, int64(ginCtx.Writer.Size()), resAttributes)
}
if cfg.recordDuration {
recorder.ObserveHTTPRequestDuration(ctx, time.Since(start), resAttributes)
}
}()
ginCtx.Next()
}
}
func computeApproximateRequestSize(r *http.Request) int64 {
s := 0
if r.URL != nil {
s = len(r.URL.Path)
}
s += len(r.Method)
s += len(r.Proto)
for name, values := range r.Header {
s += len(name)
for _, value := range values {
s += len(value)
}
}
s += len(r.Host)
// N.B. r.Form and r.MultipartForm are assumed to be included in r.URL.
if r.ContentLength != -1 {
s += int(r.ContentLength)
}
return int64(s)
}

View file

@ -0,0 +1,74 @@
package otelginmetrics
import (
"net/http"
"go.opentelemetry.io/otel/attribute"
)
// Option applies a configuration to the given config
type Option interface {
apply(cfg *config)
}
type optionFunc func(cfg *config)
func (fn optionFunc) apply(cfg *config) {
fn(cfg)
}
// WithAttributes sets a func using which what attributes to be recorded can be specified.
// By default the DefaultAttributes is used
func WithAttributes(attributes func(serverName, route string, request *http.Request) []attribute.KeyValue) Option {
return optionFunc(func(cfg *config) {
cfg.attributes = attributes
})
}
// WithRecordInFlight determines whether to record In Flight Requests or not
// By default the recordInFlight is true
func WithRecordInFlightDisabled() Option {
return optionFunc(func(cfg *config) {
cfg.recordInFlight = false
})
}
// WithRecordDuration determines whether to record Duration of Requests or not
// By default the recordDuration is true
func WithRecordDurationDisabled() Option {
return optionFunc(func(cfg *config) {
cfg.recordDuration = false
})
}
// WithRecordSize determines whether to record Size of Requests and Responses or not
// By default the recordSize is true
func WithRecordSizeDisabled() Option {
return optionFunc(func(cfg *config) {
cfg.recordSize = false
})
}
// WithGroupedStatus determines whether to group the response status codes or not. If true 2xx, 3xx will be stored
// By default the groupedStatus is true
func WithGroupedStatusDisabled() Option {
return optionFunc(func(cfg *config) {
cfg.groupedStatus = false
})
}
// WithRecorder sets a recorder for recording requests
// By default the open telemetry recorder is used
func WithRecorder(recorder Recorder) Option {
return optionFunc(func(cfg *config) {
cfg.recorder = recorder
})
}
// WithShouldRecordFunc sets a func using which whether a record should be recorded
// By default the all api calls are recorded
func WithShouldRecordFunc(shouldRecord func(serverName, route string, request *http.Request) bool) Option {
return optionFunc(func(cfg *config) {
cfg.shouldRecord = shouldRecord
})
}

View file

@ -0,0 +1,70 @@
package otelginmetrics
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
const instrumentationName = "github.com/technologize/otel-go-contrib/otelginmetrics"
// Recorder knows how to record and measure the metrics. This
// has the required methods to be used with the HTTP
// middlewares.
type otelRecorder struct {
attemptsCounter metric.Int64UpDownCounter
totalDuration metric.Int64Histogram
activeRequestsCounter metric.Int64UpDownCounter
requestSize metric.Int64Histogram
responseSize metric.Int64Histogram
}
func GetRecorder(metricsPrefix string) Recorder {
metricName := func(metricName string) string {
if len(metricsPrefix) > 0 {
return metricsPrefix + "." + metricName
}
return metricName
}
meter := otel.Meter(instrumentationName, metric.WithInstrumentationVersion(SemVersion()))
attemptsCounter, _ := meter.Int64UpDownCounter(metricName("http.server.request_count"), metric.WithDescription("Number of Requests"), metric.WithUnit("Count"))
totalDuration, _ := meter.Int64Histogram(metricName("http.server.duration"), metric.WithDescription("Time Taken by request"), metric.WithUnit("Milliseconds"))
activeRequestsCounter, _ := meter.Int64UpDownCounter(metricName("http.server.active_requests"), metric.WithDescription("Number of requests inflight"), metric.WithUnit("Count"))
requestSize, _ := meter.Int64Histogram(metricName("http.server.request_content_length"), metric.WithDescription("Request Size"), metric.WithUnit("Bytes"))
responseSize, _ := meter.Int64Histogram(metricName("http.server.response_content_length"), metric.WithDescription("Response Size"), metric.WithUnit("Bytes"))
return &otelRecorder{
attemptsCounter: attemptsCounter,
totalDuration: totalDuration,
activeRequestsCounter: activeRequestsCounter,
requestSize: requestSize,
responseSize: responseSize,
}
}
// AddRequests increments the number of requests being processed.
func (r *otelRecorder) AddRequests(ctx context.Context, quantity int64, attributes []attribute.KeyValue) {
r.attemptsCounter.Add(ctx, quantity, metric.WithAttributes(attributes...))
}
// ObserveHTTPRequestDuration measures the duration of an HTTP request.
func (r *otelRecorder) ObserveHTTPRequestDuration(ctx context.Context, duration time.Duration, attributes []attribute.KeyValue) {
r.totalDuration.Record(ctx, int64(duration/time.Millisecond), metric.WithAttributes(attributes...))
}
// ObserveHTTPRequestSize measures the size of an HTTP request in bytes.
func (r *otelRecorder) ObserveHTTPRequestSize(ctx context.Context, sizeBytes int64, attributes []attribute.KeyValue) {
r.requestSize.Record(ctx, sizeBytes, metric.WithAttributes(attributes...))
}
// ObserveHTTPResponseSize measures the size of an HTTP response in bytes.
func (r *otelRecorder) ObserveHTTPResponseSize(ctx context.Context, sizeBytes int64, attributes []attribute.KeyValue) {
r.responseSize.Record(ctx, sizeBytes, metric.WithAttributes(attributes...))
}
// AddInflightRequests increments and decrements the number of inflight request being processed.
func (r *otelRecorder) AddInflightRequests(ctx context.Context, quantity int64, attributes []attribute.KeyValue) {
r.activeRequestsCounter.Add(ctx, quantity, metric.WithAttributes(attributes...))
}

View file

@ -0,0 +1,25 @@
package otelginmetrics
import (
"context"
"time"
"go.opentelemetry.io/otel/attribute"
)
type Recorder interface {
// AddRequests increments the number of requests being processed.
AddRequests(ctx context.Context, quantity int64, attributes []attribute.KeyValue)
// ObserveHTTPRequestDuration measures the duration of an HTTP request.
ObserveHTTPRequestDuration(ctx context.Context, duration time.Duration, attributes []attribute.KeyValue)
// ObserveHTTPRequestSize measures the size of an HTTP request in bytes.
ObserveHTTPRequestSize(ctx context.Context, sizeBytes int64, attributes []attribute.KeyValue)
// ObserveHTTPResponseSize measures the size of an HTTP response in bytes.
ObserveHTTPResponseSize(ctx context.Context, sizeBytes int64, attributes []attribute.KeyValue)
// AddInflightRequests increments and decrements the number of inflight request being processed.
AddInflightRequests(ctx context.Context, quantity int64, attributes []attribute.KeyValue)
}

View file

@ -0,0 +1,11 @@
package otelginmetrics
// Version is the current release version of the gin instrumentation.
func Version() string {
return "1.0.0"
}
// SemVersion is the semantic version to be supplied to tracer/meter creation.
func SemVersion() string {
return "semver:" + Version()
}