mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 10:42:27 -05:00
155 lines
4 KiB
Go
155 lines
4 KiB
Go
|
|
// GoToSocial
|
||
|
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
|
//
|
||
|
|
// This program is free software: you can redistribute it and/or modify
|
||
|
|
// it under the terms of the GNU Affero General Public License as published by
|
||
|
|
// the Free Software Foundation, either version 3 of the License, or
|
||
|
|
// (at your option) any later version.
|
||
|
|
//
|
||
|
|
// This program is distributed in the hope that it will be useful,
|
||
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
// GNU Affero General Public License for more details.
|
||
|
|
//
|
||
|
|
// You should have received a copy of the GNU Affero General Public License
|
||
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
|
||
|
|
//go:build !nootel
|
||
|
|
|
||
|
|
package observability
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"slices"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
"go.opentelemetry.io/otel"
|
||
|
|
"go.opentelemetry.io/otel/attribute"
|
||
|
|
"go.opentelemetry.io/otel/metric"
|
||
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ginMiddleware returns a middleware that
|
||
|
|
// records metrics for incoming requests.
|
||
|
|
func ginMiddleware() gin.HandlerFunc {
|
||
|
|
meter := otel.Meter("gin", metric.WithInstrumentationVersion(config.GetSoftwareVersion()))
|
||
|
|
|
||
|
|
activeReqs, _ := meter.Int64UpDownCounter(
|
||
|
|
"http.server.requests_active",
|
||
|
|
metric.WithDescription("Number of requests currently active"),
|
||
|
|
)
|
||
|
|
|
||
|
|
totalReqs, _ := meter.Int64Counter(
|
||
|
|
"http.server.requests",
|
||
|
|
metric.WithDescription("Total number of requests served"),
|
||
|
|
)
|
||
|
|
|
||
|
|
reqSize, _ := meter.Int64Histogram(
|
||
|
|
"http.server.request_size",
|
||
|
|
metric.WithDescription("Request content length (approximate)"),
|
||
|
|
metric.WithUnit("bytes"),
|
||
|
|
)
|
||
|
|
|
||
|
|
respSize, _ := meter.Int64Histogram(
|
||
|
|
"http.server.response_size",
|
||
|
|
metric.WithDescription("Response content length"),
|
||
|
|
metric.WithUnit("bytes"),
|
||
|
|
)
|
||
|
|
|
||
|
|
duration, _ := meter.Int64Histogram(
|
||
|
|
"http.server.duration",
|
||
|
|
metric.WithDescription("Duration of request -> response"),
|
||
|
|
metric.WithUnit("ms"),
|
||
|
|
)
|
||
|
|
|
||
|
|
return func(c *gin.Context) {
|
||
|
|
|
||
|
|
ctx := c.Request.Context()
|
||
|
|
route := c.FullPath()
|
||
|
|
start := time.Now()
|
||
|
|
|
||
|
|
// Generate request attributes.
|
||
|
|
reqAttributes := []attribute.KeyValue{
|
||
|
|
semconv.HTTPServerNameKey.String("GoToSocial"),
|
||
|
|
semconv.HTTPMethodKey.String(c.Request.Method),
|
||
|
|
semconv.HTTPRouteKey.String(route),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Increment active request count,
|
||
|
|
// decrement again when we're finished.
|
||
|
|
activeReqs.Add(ctx, 1, metric.WithAttributes(reqAttributes...))
|
||
|
|
defer activeReqs.Add(ctx, -1, metric.WithAttributes(reqAttributes...))
|
||
|
|
|
||
|
|
// Process request so we can
|
||
|
|
// record response metrics.
|
||
|
|
c.Next()
|
||
|
|
|
||
|
|
// Add HTTP response code to request
|
||
|
|
// attributes to create response attributes.
|
||
|
|
respAttributes := slices.Clone(reqAttributes)
|
||
|
|
respAttributes = append(
|
||
|
|
respAttributes,
|
||
|
|
semconv.HTTPStatusCodeKey.Int(c.Writer.Status()),
|
||
|
|
)
|
||
|
|
|
||
|
|
// Increment total requests.
|
||
|
|
totalReqs.Add(ctx, 1, metric.WithAttributes(respAttributes...))
|
||
|
|
|
||
|
|
// Record request size.
|
||
|
|
reqSize.Record(
|
||
|
|
ctx,
|
||
|
|
computeApproximateRequestSize(c.Request),
|
||
|
|
metric.WithAttributes(respAttributes...),
|
||
|
|
)
|
||
|
|
|
||
|
|
// Record response size.
|
||
|
|
respSize.Record(
|
||
|
|
ctx,
|
||
|
|
int64(c.Writer.Size()),
|
||
|
|
metric.WithAttributes(respAttributes...),
|
||
|
|
)
|
||
|
|
|
||
|
|
// Record req + resp duration.
|
||
|
|
duration.Record(
|
||
|
|
ctx,
|
||
|
|
time.Since(start).Milliseconds(),
|
||
|
|
metric.WithAttributes(respAttributes...),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func computeApproximateRequestSize(r *http.Request) (sz int64) {
|
||
|
|
len := func(s string) int64 { return int64(len(s)) }
|
||
|
|
|
||
|
|
// First line
|
||
|
|
sz += len(r.Method)
|
||
|
|
if r.URL != nil {
|
||
|
|
sz += len(r.URL.Path)
|
||
|
|
sz++ // for '?'
|
||
|
|
sz += len(r.URL.RawQuery)
|
||
|
|
sz += len(r.URL.RawFragment)
|
||
|
|
}
|
||
|
|
sz += len(r.Proto)
|
||
|
|
sz += 2 // for '\r\n'
|
||
|
|
|
||
|
|
// Next are lines for request headers.
|
||
|
|
for name, values := range r.Header {
|
||
|
|
|
||
|
|
// Each value on its own line.
|
||
|
|
for _, value := range values {
|
||
|
|
sz += len(name)
|
||
|
|
sz += len(value)
|
||
|
|
sz += 2 // for '\r\n'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Finally, any request body (if set),
|
||
|
|
// this includes (multipart) form data.
|
||
|
|
sz += max(r.ContentLength, 0)
|
||
|
|
|
||
|
|
return
|
||
|
|
}
|