[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

@ -140,6 +140,11 @@ type Configuration struct {
TracingEndpoint string `name:"tracing-endpoint" usage:"Endpoint of your trace collector. Eg., 'localhost:4317' for gRPC, 'localhost:4318' for http"`
TracingInsecureTransport bool `name:"tracing-insecure-transport" usage:"Disable TLS for the gRPC or HTTP transport protocol"`
MetricsEnabled bool `name:"metrics-enabled" usage:"Enable OpenTelemetry based metrics support."`
MetricsAuthEnabled bool `name:"metrics-auth-enabled" usage:"Enable HTTP Basic Authentication for Prometheus metrics endpoint"`
MetricsAuthUsername string `name:"metrics-auth-username" usage:"Username for Prometheus metrics endpoint"`
MetricsAuthPassword string `name:"metrics-auth-password" usage:"Password for Prometheus metrics endpoint"`
SMTPHost string `name:"smtp-host" usage:"Host of the smtp server. Eg., 'smtp.eu.mailgun.org'"`
SMTPPort int `name:"smtp-port" usage:"Port of the smtp server. Eg., 587"`
SMTPUsername string `name:"smtp-username" usage:"Username to authenticate with the smtp server as. Eg., 'postmaster@mail.example.org'"`

View file

@ -121,6 +121,9 @@ var Defaults = Configuration{
TracingEndpoint: "",
TracingInsecureTransport: false,
MetricsEnabled: false,
MetricsAuthEnabled: false,
SyslogEnabled: false,
SyslogProtocol: "udp",
SyslogAddress: "localhost:514",

View file

@ -2100,6 +2100,106 @@ func GetTracingInsecureTransport() bool { return global.GetTracingInsecureTransp
// SetTracingInsecureTransport safely sets the value for global configuration 'TracingInsecureTransport' field
func SetTracingInsecureTransport(v bool) { global.SetTracingInsecureTransport(v) }
// GetMetricsEnabled safely fetches the Configuration value for state's 'MetricsEnabled' field
func (st *ConfigState) GetMetricsEnabled() (v bool) {
st.mutex.RLock()
v = st.config.MetricsEnabled
st.mutex.RUnlock()
return
}
// SetMetricsEnabled safely sets the Configuration value for state's 'MetricsEnabled' field
func (st *ConfigState) SetMetricsEnabled(v bool) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.MetricsEnabled = v
st.reloadToViper()
}
// MetricsEnabledFlag returns the flag name for the 'MetricsEnabled' field
func MetricsEnabledFlag() string { return "metrics-enabled" }
// GetMetricsEnabled safely fetches the value for global configuration 'MetricsEnabled' field
func GetMetricsEnabled() bool { return global.GetMetricsEnabled() }
// SetMetricsEnabled safely sets the value for global configuration 'MetricsEnabled' field
func SetMetricsEnabled(v bool) { global.SetMetricsEnabled(v) }
// GetMetricsAuthEnabled safely fetches the Configuration value for state's 'MetricsAuthEnabled' field
func (st *ConfigState) GetMetricsAuthEnabled() (v bool) {
st.mutex.RLock()
v = st.config.MetricsAuthEnabled
st.mutex.RUnlock()
return
}
// SetMetricsAuthEnabled safely sets the Configuration value for state's 'MetricsAuthEnabled' field
func (st *ConfigState) SetMetricsAuthEnabled(v bool) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.MetricsAuthEnabled = v
st.reloadToViper()
}
// MetricsAuthEnabledFlag returns the flag name for the 'MetricsAuthEnabled' field
func MetricsAuthEnabledFlag() string { return "metrics-auth-enabled" }
// GetMetricsAuthEnabled safely fetches the value for global configuration 'MetricsAuthEnabled' field
func GetMetricsAuthEnabled() bool { return global.GetMetricsAuthEnabled() }
// SetMetricsAuthEnabled safely sets the value for global configuration 'MetricsAuthEnabled' field
func SetMetricsAuthEnabled(v bool) { global.SetMetricsAuthEnabled(v) }
// GetMetricsAuthUsername safely fetches the Configuration value for state's 'MetricsAuthUsername' field
func (st *ConfigState) GetMetricsAuthUsername() (v string) {
st.mutex.RLock()
v = st.config.MetricsAuthUsername
st.mutex.RUnlock()
return
}
// SetMetricsAuthUsername safely sets the Configuration value for state's 'MetricsAuthUsername' field
func (st *ConfigState) SetMetricsAuthUsername(v string) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.MetricsAuthUsername = v
st.reloadToViper()
}
// MetricsAuthUsernameFlag returns the flag name for the 'MetricsAuthUsername' field
func MetricsAuthUsernameFlag() string { return "metrics-auth-username" }
// GetMetricsAuthUsername safely fetches the value for global configuration 'MetricsAuthUsername' field
func GetMetricsAuthUsername() string { return global.GetMetricsAuthUsername() }
// SetMetricsAuthUsername safely sets the value for global configuration 'MetricsAuthUsername' field
func SetMetricsAuthUsername(v string) { global.SetMetricsAuthUsername(v) }
// GetMetricsAuthPassword safely fetches the Configuration value for state's 'MetricsAuthPassword' field
func (st *ConfigState) GetMetricsAuthPassword() (v string) {
st.mutex.RLock()
v = st.config.MetricsAuthPassword
st.mutex.RUnlock()
return
}
// SetMetricsAuthPassword safely sets the Configuration value for state's 'MetricsAuthPassword' field
func (st *ConfigState) SetMetricsAuthPassword(v string) {
st.mutex.Lock()
defer st.mutex.Unlock()
st.config.MetricsAuthPassword = v
st.reloadToViper()
}
// MetricsAuthPasswordFlag returns the flag name for the 'MetricsAuthPassword' field
func MetricsAuthPasswordFlag() string { return "metrics-auth-password" }
// GetMetricsAuthPassword safely fetches the value for global configuration 'MetricsAuthPassword' field
func GetMetricsAuthPassword() string { return global.GetMetricsAuthPassword() }
// SetMetricsAuthPassword safely sets the value for global configuration 'MetricsAuthPassword' field
func SetMetricsAuthPassword(v string) { global.SetMetricsAuthPassword(v) }
// GetSMTPHost safely fetches the Configuration value for state's 'SMTPHost' field
func (st *ConfigState) GetSMTPHost() (v string) {
st.mutex.RLock()

View file

@ -40,6 +40,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/metrics"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/tracing"
"github.com/uptrace/bun"
@ -142,6 +143,9 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
if config.GetTracingEnabled() {
db.AddQueryHook(tracing.InstrumentBun())
}
if config.GetMetricsEnabled() {
db.AddQueryHook(metrics.InstrumentBun())
}
// table registration is needed for many-to-many, see:
// https://bun.uptrace.dev/orm/many-to-many-relation/

View file

@ -0,0 +1,82 @@
// 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 !nometrics
package metrics
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/technologize/otel-go-contrib/otelginmetrics"
"github.com/uptrace/bun"
"github.com/uptrace/bun/extra/bunotel"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/prometheus"
sdk "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
const (
serviceName = "GoToSocial"
)
func Initialize() error {
if !config.GetMetricsEnabled() {
return nil
}
if config.GetMetricsAuthEnabled() {
if config.GetMetricsAuthPassword() == "" || config.GetMetricsAuthUsername() == "" {
return errors.New("metrics-auth-username and metrics-auth-password must be set when metrics-auth-enabled is true")
}
}
r, _ := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
),
)
prometheusExporter, err := prometheus.New()
if err != nil {
return err
}
meterProvider := sdk.NewMeterProvider(
sdk.WithResource(r),
sdk.WithReader(prometheusExporter),
)
otel.SetMeterProvider(meterProvider)
return nil
}
func InstrumentGin() gin.HandlerFunc {
return otelginmetrics.Middleware(serviceName)
}
func InstrumentBun() bun.QueryHook {
return bunotel.NewQueryHook(
bunotel.WithMeterProvider(otel.GetMeterProvider()),
)
}

View file

@ -0,0 +1,43 @@
// 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 nometrics
package metrics
import (
"errors"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/uptrace/bun"
)
func Initialize() error {
if config.GetMetricsEnabled() {
return errors.New("metrics was disabled at build time")
}
return nil
}
func InstrumentGin() gin.HandlerFunc {
return func(c *gin.Context) {}
}
func InstrumentBun() bun.QueryHook {
return nil
}

33
internal/web/metrics.go Normal file
View file

@ -0,0 +1,33 @@
// 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/>.
package web
import (
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
metricsPath = "/metrics"
metricsUser = "metrics"
)
func (m *Module) metricsGETHandler(c *gin.Context) {
h := promhttp.Handler()
h.ServeHTTP(c.Writer, c.Request)
}

View file

@ -110,6 +110,19 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
r.AttachHandler(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler)
r.AttachHandler(http.MethodGet, tagsPath, m.tagGETHandler)
// Prometheus metrics export endpoint
if config.GetMetricsEnabled() {
metricsGroup := r.AttachGroup(metricsPath)
metricsGroup.Use(mi...)
// Attach basic auth if enabled
if config.GetMetricsAuthEnabled() {
metricsGroup.Use(gin.BasicAuth(gin.Accounts{
config.GetMetricsAuthUsername(): config.GetMetricsAuthPassword(),
}))
}
metricsGroup.Handle(http.MethodGet, "", m.metricsGETHandler)
}
// Attach redirects from old endpoints to current ones for backwards compatibility
r.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })
r.AttachHandler(http.MethodGet, "/user", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })