mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
feat(metrics): add metrics server (#1930)
* feat(metrics): add metrics server * chore: update license headers * feat(metrics): add optional basic auth * feat(metrics): add go and process collectors --------- Co-authored-by: ze0s <43699394+zze0s@users.noreply.github.com> Co-authored-by: ze0s <ze0s@riseup.net>
This commit is contained in:
parent
0d5902c8f6
commit
3f8bc0140c
16 changed files with 1191 additions and 83 deletions
|
@ -126,7 +126,29 @@ sessionSecret = "{{ .sessionSecret }}"
|
|||
# OIDC Redirect URL (e.g. http://localhost:7474/api/auth/oidc/callback)
|
||||
#oidc_redirect_url = ""
|
||||
|
||||
# Metrics
|
||||
#
|
||||
# Enable metrics endpoint
|
||||
#metricsEnabled = true
|
||||
#
|
||||
# Metrics server host
|
||||
#
|
||||
#metricsHost = "127.0.0.1"
|
||||
#
|
||||
# Metrics server port
|
||||
#
|
||||
#metricsPort = 9074
|
||||
#
|
||||
# Metrics basic auth
|
||||
#
|
||||
# Comma separate list of user:password. Password must be htpasswd bcrypt hashed. Use autobrrctl to generate.
|
||||
# Only enabled if correctly set with user:pass.
|
||||
#
|
||||
#metricsBasicAuthUsers = ""
|
||||
|
||||
# Custom definitions
|
||||
#
|
||||
#customDefinitions = "test/definitions"
|
||||
`
|
||||
|
||||
func (c *AppConfig) writeConfig(configPath string, configFile string) error {
|
||||
|
@ -242,30 +264,34 @@ func New(configPath string, version string) *AppConfig {
|
|||
|
||||
func (c *AppConfig) defaults() {
|
||||
c.Config = &domain.Config{
|
||||
Version: "dev",
|
||||
Host: "localhost",
|
||||
Port: 7474,
|
||||
LogLevel: "TRACE",
|
||||
LogPath: "",
|
||||
LogMaxSize: 50,
|
||||
LogMaxBackups: 3,
|
||||
DatabaseMaxBackups: 5,
|
||||
BaseURL: "/",
|
||||
BaseURLModeLegacy: true,
|
||||
SessionSecret: api.GenerateSecureToken(16),
|
||||
CustomDefinitions: "",
|
||||
CheckForUpdates: true,
|
||||
DatabaseType: "sqlite",
|
||||
PostgresHost: "",
|
||||
PostgresPort: 0,
|
||||
PostgresDatabase: "",
|
||||
PostgresUser: "",
|
||||
PostgresPass: "",
|
||||
PostgresSSLMode: "disable",
|
||||
PostgresExtraParams: "",
|
||||
ProfilingEnabled: false,
|
||||
ProfilingHost: "127.0.0.1",
|
||||
ProfilingPort: 6060,
|
||||
Version: "dev",
|
||||
Host: "localhost",
|
||||
Port: 7474,
|
||||
LogLevel: "TRACE",
|
||||
LogPath: "",
|
||||
LogMaxSize: 50,
|
||||
LogMaxBackups: 3,
|
||||
DatabaseMaxBackups: 5,
|
||||
BaseURL: "/",
|
||||
BaseURLModeLegacy: true,
|
||||
SessionSecret: api.GenerateSecureToken(16),
|
||||
CustomDefinitions: "",
|
||||
CheckForUpdates: true,
|
||||
DatabaseType: "sqlite",
|
||||
PostgresHost: "",
|
||||
PostgresPort: 0,
|
||||
PostgresDatabase: "",
|
||||
PostgresUser: "",
|
||||
PostgresPass: "",
|
||||
PostgresSSLMode: "disable",
|
||||
PostgresExtraParams: "",
|
||||
ProfilingEnabled: false,
|
||||
ProfilingHost: "127.0.0.1",
|
||||
ProfilingPort: 6060,
|
||||
MetricsEnabled: false,
|
||||
MetricsHost: "127.0.0.1",
|
||||
MetricsPort: 9074,
|
||||
MetricsBasicAuthUsers: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -405,6 +431,25 @@ func (c *AppConfig) loadFromEnv() {
|
|||
if v := os.Getenv(prefix + "OIDC_REDIRECT_URL"); v != "" {
|
||||
c.Config.OIDCRedirectURL = v
|
||||
}
|
||||
|
||||
if v := os.Getenv(prefix + "METRICS_ENABLED"); v != "" {
|
||||
c.Config.MetricsEnabled = strings.EqualFold(strings.ToLower(v), "true")
|
||||
}
|
||||
|
||||
if v := os.Getenv(prefix + "METRICS_HOST"); v != "" {
|
||||
c.Config.MetricsHost = v
|
||||
}
|
||||
|
||||
if v := os.Getenv(prefix + "METRICS_PORT"); v != "" {
|
||||
i, _ := strconv.ParseInt(v, 10, 32)
|
||||
if i > 0 {
|
||||
c.Config.MetricsPort = int(i)
|
||||
}
|
||||
}
|
||||
|
||||
if v := os.Getenv(prefix + "METRICS_BASIC_AUTH_USERS"); v != "" {
|
||||
c.Config.MetricsBasicAuthUsers = v
|
||||
}
|
||||
}
|
||||
|
||||
func validDatabaseType(v string) bool {
|
||||
|
|
|
@ -4,37 +4,41 @@
|
|||
package domain
|
||||
|
||||
type Config struct {
|
||||
Version string
|
||||
ConfigPath string
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
LogLevel string `toml:"logLevel"`
|
||||
LogPath string `toml:"logPath"`
|
||||
LogMaxSize int `toml:"logMaxSize"`
|
||||
LogMaxBackups int `toml:"logMaxBackups"`
|
||||
BaseURL string `toml:"baseUrl"`
|
||||
BaseURLModeLegacy bool `toml:"baseUrlModeLegacy"`
|
||||
SessionSecret string `toml:"sessionSecret"`
|
||||
CustomDefinitions string `toml:"customDefinitions"`
|
||||
CheckForUpdates bool `toml:"checkForUpdates"`
|
||||
DatabaseType string `toml:"databaseType"`
|
||||
DatabaseMaxBackups int `toml:"databaseMaxBackups"`
|
||||
PostgresHost string `toml:"postgresHost"`
|
||||
PostgresPort int `toml:"postgresPort"`
|
||||
PostgresDatabase string `toml:"postgresDatabase"`
|
||||
PostgresUser string `toml:"postgresUser"`
|
||||
PostgresPass string `toml:"postgresPass"`
|
||||
PostgresSSLMode string `toml:"postgresSSLMode"`
|
||||
PostgresExtraParams string `toml:"postgresExtraParams"`
|
||||
ProfilingEnabled bool `toml:"profilingEnabled"`
|
||||
ProfilingHost string `toml:"profilingHost"`
|
||||
ProfilingPort int `toml:"profilingPort"`
|
||||
OIDCEnabled bool `mapstructure:"oidc_enabled"`
|
||||
OIDCIssuer string `mapstructure:"oidc_issuer"`
|
||||
OIDCClientID string `mapstructure:"oidc_client_id"`
|
||||
OIDCClientSecret string `mapstructure:"oidc_client_secret"`
|
||||
OIDCRedirectURL string `mapstructure:"oidc_redirect_url"`
|
||||
OIDCScopes string `mapstructure:"oidc_scopes"`
|
||||
Version string
|
||||
ConfigPath string
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
LogLevel string `toml:"logLevel"`
|
||||
LogPath string `toml:"logPath"`
|
||||
LogMaxSize int `toml:"logMaxSize"`
|
||||
LogMaxBackups int `toml:"logMaxBackups"`
|
||||
BaseURL string `toml:"baseUrl"`
|
||||
BaseURLModeLegacy bool `toml:"baseUrlModeLegacy"`
|
||||
SessionSecret string `toml:"sessionSecret"`
|
||||
CustomDefinitions string `toml:"customDefinitions"`
|
||||
CheckForUpdates bool `toml:"checkForUpdates"`
|
||||
DatabaseType string `toml:"databaseType"`
|
||||
DatabaseMaxBackups int `toml:"databaseMaxBackups"`
|
||||
PostgresHost string `toml:"postgresHost"`
|
||||
PostgresPort int `toml:"postgresPort"`
|
||||
PostgresDatabase string `toml:"postgresDatabase"`
|
||||
PostgresUser string `toml:"postgresUser"`
|
||||
PostgresPass string `toml:"postgresPass"`
|
||||
PostgresSSLMode string `toml:"postgresSSLMode"`
|
||||
PostgresExtraParams string `toml:"postgresExtraParams"`
|
||||
ProfilingEnabled bool `toml:"profilingEnabled"`
|
||||
ProfilingHost string `toml:"profilingHost"`
|
||||
ProfilingPort int `toml:"profilingPort"`
|
||||
OIDCEnabled bool `mapstructure:"oidc_enabled"`
|
||||
OIDCIssuer string `mapstructure:"oidc_issuer"`
|
||||
OIDCClientID string `mapstructure:"oidc_client_id"`
|
||||
OIDCClientSecret string `mapstructure:"oidc_client_secret"`
|
||||
OIDCRedirectURL string `mapstructure:"oidc_redirect_url"`
|
||||
OIDCScopes string `mapstructure:"oidc_scopes"`
|
||||
MetricsEnabled bool `toml:"metricsEnabled"`
|
||||
MetricsHost string `toml:"metricsHost"`
|
||||
MetricsPort int `toml:"metricsPort"`
|
||||
MetricsBasicAuthUsers string `toml:"metricsBasicAuthUsers"`
|
||||
}
|
||||
|
||||
type ConfigUpdate struct {
|
||||
|
|
96
internal/http/metrics_server.go
Normal file
96
internal/http/metrics_server.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/config"
|
||||
"github.com/autobrr/autobrr/internal/logger"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type metricsManager interface {
|
||||
GetRegistry() *prometheus.Registry
|
||||
}
|
||||
|
||||
type MetricsServer struct {
|
||||
log zerolog.Logger
|
||||
|
||||
config *config.AppConfig
|
||||
|
||||
version string
|
||||
commit string
|
||||
date string
|
||||
|
||||
metricsManager metricsManager
|
||||
}
|
||||
|
||||
func NewMetricsServer(log logger.Logger, config *config.AppConfig, version string, commit string, date string, metricsManager metricsManager) MetricsServer {
|
||||
return MetricsServer{
|
||||
log: log.With().Str("module", "http").Logger(),
|
||||
config: config,
|
||||
version: version,
|
||||
commit: commit,
|
||||
date: date,
|
||||
|
||||
metricsManager: metricsManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (s MetricsServer) Open() error {
|
||||
addr := fmt.Sprintf("%v:%v", s.config.Config.MetricsHost, s.config.Config.MetricsPort)
|
||||
|
||||
var err error
|
||||
for _, proto := range []string{"tcp", "tcp4", "tcp6"} {
|
||||
if err = s.tryToServe(addr, proto); err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
s.log.Error().Err(err).Msgf("Failed to start %s server. Attempted to listen on %s", proto, addr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s MetricsServer) tryToServe(addr, protocol string) error {
|
||||
listener, err := net.Listen(protocol, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.log.Info().Msgf("Starting Metrics %s server. Listening on %s", protocol, listener.Addr().String())
|
||||
|
||||
server := http.Server{
|
||||
Handler: s.Handler(),
|
||||
ReadHeaderTimeout: time.Second * 15,
|
||||
}
|
||||
|
||||
return server.Serve(listener)
|
||||
}
|
||||
|
||||
func (s MetricsServer) Handler() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(LoggerMiddleware(&s.log))
|
||||
|
||||
if s.config.Config.MetricsBasicAuthUsers != "" {
|
||||
r.Use(BasicAuth("metrics", s.config.Config.MetricsBasicAuthUsers))
|
||||
}
|
||||
|
||||
r.Get("/metrics", promhttp.HandlerFor(s.metricsManager.GetRegistry(), promhttp.HandlerOpts{}).ServeHTTP)
|
||||
|
||||
return r
|
||||
}
|
|
@ -5,6 +5,7 @@ package http
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
@ -12,6 +13,7 @@ import (
|
|||
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (s Server) IsAuthenticated(next http.Handler) http.Handler {
|
||||
|
@ -133,3 +135,45 @@ func LoggerMiddleware(logger *zerolog.Logger) func(next http.Handler) http.Handl
|
|||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
// BasicAuth implements a simple middleware handler for adding basic http auth to a route.
|
||||
func BasicAuth(realm string, users string) func(next http.Handler) http.Handler {
|
||||
creds := map[string]string{}
|
||||
|
||||
userCreds := strings.Split(users, ",")
|
||||
for _, cred := range userCreds {
|
||||
credParts := strings.Split(cred, ":")
|
||||
if len(credParts) != 2 {
|
||||
//s.log.Warn().Msgf("Invalid metrics basic auth credentials: %s", cred)
|
||||
continue
|
||||
}
|
||||
|
||||
creds[credParts[0]] = credParts[1]
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
basicAuthFailed(w, realm)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate username and password using htpasswd data
|
||||
if hashedPassword, exists := creds[username]; exists {
|
||||
// Use bcrypt to validate the password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)); err == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
basicAuthFailed(w, realm)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func basicAuthFailed(w http.ResponseWriter, realm string) {
|
||||
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
|
|
89
internal/metrics/collector/feed.go
Normal file
89
internal/metrics/collector/feed.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/feed"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type feedCollector struct {
|
||||
feedService feed.Service
|
||||
|
||||
totalCount *prometheus.Desc
|
||||
enabledCount *prometheus.Desc
|
||||
LastRunTimestamp *prometheus.Desc
|
||||
NextRunTimestamp *prometheus.Desc
|
||||
errorMetric *prometheus.Desc
|
||||
}
|
||||
|
||||
func (collector *feedCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.totalCount
|
||||
ch <- collector.enabledCount
|
||||
ch <- collector.LastRunTimestamp
|
||||
ch <- collector.NextRunTimestamp
|
||||
ch <- collector.errorMetric
|
||||
}
|
||||
|
||||
func (collector *feedCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
feeds, err := collector.feedService.Find(context.TODO())
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
enabled := 0
|
||||
for _, f := range feeds {
|
||||
if f.Enabled {
|
||||
enabled++
|
||||
}
|
||||
|
||||
if !f.LastRun.IsZero() {
|
||||
ch <- prometheus.MustNewConstMetric(collector.LastRunTimestamp, prometheus.GaugeValue, float64(int(f.LastRun.Unix())), f.Name)
|
||||
}
|
||||
if !f.NextRun.IsZero() {
|
||||
ch <- prometheus.MustNewConstMetric(collector.NextRunTimestamp, prometheus.GaugeValue, float64(int(f.NextRun.Unix())), f.Name)
|
||||
}
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(collector.totalCount, prometheus.GaugeValue, float64(len(feeds)))
|
||||
ch <- prometheus.MustNewConstMetric(collector.enabledCount, prometheus.GaugeValue, float64(enabled))
|
||||
}
|
||||
|
||||
func NewFeedCollector(feedService feed.Service) *feedCollector {
|
||||
return &feedCollector{
|
||||
feedService: feedService,
|
||||
totalCount: prometheus.NewDesc(
|
||||
"autobrr_feed_total",
|
||||
"Number of feeds",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
enabledCount: prometheus.NewDesc(
|
||||
"autobrr_feed_enabled_total",
|
||||
"Number of enabled feeds",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
LastRunTimestamp: prometheus.NewDesc(
|
||||
"autobrr_feed_last_run_timestamp_seconds",
|
||||
"The timestamp of the last feed run",
|
||||
[]string{"feed"},
|
||||
nil,
|
||||
),
|
||||
NextRunTimestamp: prometheus.NewDesc(
|
||||
"autobrr_feed_next_run_timestamp_seconds",
|
||||
"The timestamp of the next feed run",
|
||||
[]string{"feed"},
|
||||
nil,
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
"autobrr_feed_collector_error",
|
||||
"Error while collecting feed metrics",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
66
internal/metrics/collector/filter.go
Normal file
66
internal/metrics/collector/filter.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/filter"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type filterCollector struct {
|
||||
filterService filter.Service
|
||||
|
||||
totalCount *prometheus.Desc
|
||||
enabledCount *prometheus.Desc
|
||||
errorMetric *prometheus.Desc
|
||||
}
|
||||
|
||||
func (collector *filterCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.totalCount
|
||||
ch <- collector.enabledCount
|
||||
ch <- collector.errorMetric
|
||||
}
|
||||
|
||||
func (collector *filterCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
lists, err := collector.filterService.ListFilters(context.TODO())
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
enabled := 0
|
||||
for _, f := range lists {
|
||||
if f.Enabled {
|
||||
enabled++
|
||||
}
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(collector.totalCount, prometheus.GaugeValue, float64(len(lists)))
|
||||
ch <- prometheus.MustNewConstMetric(collector.enabledCount, prometheus.GaugeValue, float64(enabled))
|
||||
}
|
||||
|
||||
func NewFilterCollector(filterService filter.Service) *filterCollector {
|
||||
return &filterCollector{
|
||||
filterService: filterService,
|
||||
totalCount: prometheus.NewDesc(
|
||||
"autobrr_filter_total",
|
||||
"Number of filters",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
enabledCount: prometheus.NewDesc(
|
||||
"autobrr_filter_enabled_total",
|
||||
"Number of enabled filters",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
"autobrr_filter_collector_error",
|
||||
"Error while collecting filter metrics",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
140
internal/metrics/collector/irc.go
Normal file
140
internal/metrics/collector/irc.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/irc"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type ircCollector struct {
|
||||
ircService irc.Service
|
||||
|
||||
totalCount *prometheus.Desc
|
||||
enabledCount *prometheus.Desc
|
||||
connectedCount *prometheus.Desc
|
||||
healthyCount *prometheus.Desc
|
||||
channelCount *prometheus.Desc
|
||||
channelEnabledCount *prometheus.Desc
|
||||
channelMonitoringCount *prometheus.Desc
|
||||
channelLastAnnouncedTimestamp *prometheus.Desc
|
||||
errorMetric *prometheus.Desc
|
||||
}
|
||||
|
||||
func (collector *ircCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.totalCount
|
||||
ch <- collector.enabledCount
|
||||
ch <- collector.connectedCount
|
||||
ch <- collector.channelCount
|
||||
ch <- collector.channelEnabledCount
|
||||
ch <- collector.channelMonitoringCount
|
||||
ch <- collector.channelLastAnnouncedTimestamp
|
||||
ch <- collector.errorMetric
|
||||
}
|
||||
|
||||
func (collector *ircCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
networks, err := collector.ircService.GetNetworksWithHealth(context.TODO())
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
enabled := 0
|
||||
healthy := 0
|
||||
connected := 0
|
||||
for _, n := range networks {
|
||||
if n.Enabled {
|
||||
enabled++
|
||||
}
|
||||
if n.Connected {
|
||||
connected++
|
||||
}
|
||||
if n.Healthy {
|
||||
healthy++
|
||||
}
|
||||
|
||||
channelsEnabled := 0
|
||||
channelsMonitoring := 0
|
||||
for _, c := range n.Channels {
|
||||
if c.Enabled {
|
||||
channelsEnabled++
|
||||
}
|
||||
if c.Monitoring {
|
||||
channelsMonitoring++
|
||||
}
|
||||
if !c.LastAnnounce.IsZero() {
|
||||
ch <- prometheus.MustNewConstMetric(collector.channelLastAnnouncedTimestamp, prometheus.GaugeValue, float64(int(c.LastAnnounce.Unix())), n.Name, c.Name)
|
||||
}
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(collector.channelCount, prometheus.GaugeValue, float64(len(n.Channels)), n.Name)
|
||||
ch <- prometheus.MustNewConstMetric(collector.channelEnabledCount, prometheus.GaugeValue, float64(channelsEnabled), n.Name)
|
||||
ch <- prometheus.MustNewConstMetric(collector.channelMonitoringCount, prometheus.GaugeValue, float64(channelsMonitoring), n.Name)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(collector.totalCount, prometheus.GaugeValue, float64(len(networks)))
|
||||
ch <- prometheus.MustNewConstMetric(collector.enabledCount, prometheus.GaugeValue, float64(enabled))
|
||||
ch <- prometheus.MustNewConstMetric(collector.connectedCount, prometheus.GaugeValue, float64(connected))
|
||||
ch <- prometheus.MustNewConstMetric(collector.healthyCount, prometheus.GaugeValue, float64(healthy))
|
||||
}
|
||||
|
||||
func NewIRCCollector(ircService irc.Service) *ircCollector {
|
||||
return &ircCollector{
|
||||
ircService: ircService,
|
||||
totalCount: prometheus.NewDesc(
|
||||
"autobrr_irc_total",
|
||||
"Number of IRC networks",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
enabledCount: prometheus.NewDesc(
|
||||
"autobrr_irc_enabled_total",
|
||||
"Number of enabled IRC networks",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
connectedCount: prometheus.NewDesc(
|
||||
"autobrr_irc_connected_total",
|
||||
"Number of connected IRC networks",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
healthyCount: prometheus.NewDesc(
|
||||
"autobrr_irc_healthy_total",
|
||||
"Number of healthy IRC networks",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
channelCount: prometheus.NewDesc(
|
||||
"autobrr_irc_channel_total",
|
||||
"Number of IRC channel",
|
||||
[]string{"network"},
|
||||
nil,
|
||||
),
|
||||
channelEnabledCount: prometheus.NewDesc(
|
||||
"autobrr_irc_channel_enabled_total",
|
||||
"Number of enabled IRC channel",
|
||||
[]string{"network"},
|
||||
nil,
|
||||
),
|
||||
channelMonitoringCount: prometheus.NewDesc(
|
||||
"autobrr_irc_channel_monitored_total",
|
||||
"Number of IRC channel monitored",
|
||||
[]string{"network"},
|
||||
nil,
|
||||
),
|
||||
channelLastAnnouncedTimestamp: prometheus.NewDesc(
|
||||
"autobrr_irc_channel_last_announced_timestamp_seconds",
|
||||
"The timestamp of the last announced release",
|
||||
[]string{"network", "channel"},
|
||||
nil,
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
"autobrr_irc_collector_error",
|
||||
"Error while collecting irc metrics",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
78
internal/metrics/collector/list.go
Normal file
78
internal/metrics/collector/list.go
Normal file
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/list"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type listCollector struct {
|
||||
listService list.Service
|
||||
|
||||
totalCount *prometheus.Desc
|
||||
enabledCount *prometheus.Desc
|
||||
LastRefreshTimestamp *prometheus.Desc
|
||||
errorMetric *prometheus.Desc
|
||||
}
|
||||
|
||||
func (collector *listCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.totalCount
|
||||
ch <- collector.enabledCount
|
||||
ch <- collector.LastRefreshTimestamp
|
||||
ch <- collector.errorMetric
|
||||
}
|
||||
|
||||
func (collector *listCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
lists, err := collector.listService.List(context.TODO())
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
enabled := 0
|
||||
for _, l := range lists {
|
||||
if l.Enabled {
|
||||
enabled++
|
||||
}
|
||||
|
||||
if !l.LastRefreshTime.IsZero() {
|
||||
ch <- prometheus.MustNewConstMetric(collector.LastRefreshTimestamp, prometheus.GaugeValue, float64(int(l.LastRefreshTime.Unix())), l.Name)
|
||||
}
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(collector.totalCount, prometheus.GaugeValue, float64(len(lists)))
|
||||
ch <- prometheus.MustNewConstMetric(collector.enabledCount, prometheus.GaugeValue, float64(enabled))
|
||||
}
|
||||
|
||||
func NewListCollector(listService list.Service) *listCollector {
|
||||
return &listCollector{
|
||||
listService: listService,
|
||||
totalCount: prometheus.NewDesc(
|
||||
"autobrr_list_total",
|
||||
"Number of lists",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
enabledCount: prometheus.NewDesc(
|
||||
"autobrr_list_enabled_total",
|
||||
"Number of enabled lists",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
LastRefreshTimestamp: prometheus.NewDesc(
|
||||
"autobrr_list_last_refresh_timestamp_seconds",
|
||||
"The timestamp of the last list run",
|
||||
[]string{"list"},
|
||||
nil,
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
"autobrr_list_collector_error",
|
||||
"Error while collecting list metrics",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
96
internal/metrics/collector/release.go
Normal file
96
internal/metrics/collector/release.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/autobrr/autobrr/internal/release"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type releaseCollector struct {
|
||||
releaseService release.Service
|
||||
|
||||
totalCount *prometheus.Desc
|
||||
filteredCount *prometheus.Desc
|
||||
filterRejectedCount *prometheus.Desc
|
||||
pushApprovedCount *prometheus.Desc
|
||||
pushRejectedCount *prometheus.Desc
|
||||
pushErrorCount *prometheus.Desc
|
||||
errorMetric *prometheus.Desc
|
||||
}
|
||||
|
||||
func (collector *releaseCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- collector.totalCount
|
||||
ch <- collector.filteredCount
|
||||
ch <- collector.filterRejectedCount
|
||||
ch <- collector.pushApprovedCount
|
||||
ch <- collector.pushRejectedCount
|
||||
ch <- collector.pushErrorCount
|
||||
ch <- collector.errorMetric
|
||||
}
|
||||
|
||||
func (collector *releaseCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
stats, err := collector.releaseService.Stats(context.TODO())
|
||||
if err != nil {
|
||||
ch <- prometheus.NewInvalidMetric(collector.errorMetric, err)
|
||||
return
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(collector.totalCount, prometheus.GaugeValue, float64(stats.TotalCount))
|
||||
ch <- prometheus.MustNewConstMetric(collector.filteredCount, prometheus.GaugeValue, float64(stats.FilteredCount))
|
||||
ch <- prometheus.MustNewConstMetric(collector.filterRejectedCount, prometheus.GaugeValue, float64(stats.FilterRejectedCount))
|
||||
ch <- prometheus.MustNewConstMetric(collector.pushApprovedCount, prometheus.GaugeValue, float64(stats.PushApprovedCount))
|
||||
ch <- prometheus.MustNewConstMetric(collector.pushRejectedCount, prometheus.GaugeValue, float64(stats.PushRejectedCount))
|
||||
ch <- prometheus.MustNewConstMetric(collector.pushErrorCount, prometheus.GaugeValue, float64(stats.PushErrorCount))
|
||||
}
|
||||
|
||||
func NewReleaseCollector(releaseService release.Service) *releaseCollector {
|
||||
return &releaseCollector{
|
||||
releaseService: releaseService,
|
||||
totalCount: prometheus.NewDesc(
|
||||
"autobrr_release_total",
|
||||
"Number of releases",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
filteredCount: prometheus.NewDesc(
|
||||
"autobrr_release_filtered_total",
|
||||
"Number of releases filtered",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
filterRejectedCount: prometheus.NewDesc(
|
||||
"autobrr_release_filter_rejected_total",
|
||||
"Number of releases that got rejected because of a filter",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
pushApprovedCount: prometheus.NewDesc(
|
||||
"autobrr_release_push_approved_total",
|
||||
"Number of releases push approved",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
pushRejectedCount: prometheus.NewDesc(
|
||||
"autobrr_release_push_rejected_total",
|
||||
"Number of releases push rejected",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
pushErrorCount: prometheus.NewDesc(
|
||||
"autobrr_release_push_error_total",
|
||||
"Number of releases push errored",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
errorMetric: prometheus.NewDesc(
|
||||
"autobrr_release_collector_error",
|
||||
"Error while collecting release metrics",
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
52
internal/metrics/metrics.go
Normal file
52
internal/metrics/metrics.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) 2021 - 2025, Ludvig Lundgren and the autobrr contributors.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/autobrr/autobrr/internal/feed"
|
||||
"github.com/autobrr/autobrr/internal/filter"
|
||||
"github.com/autobrr/autobrr/internal/irc"
|
||||
"github.com/autobrr/autobrr/internal/list"
|
||||
"github.com/autobrr/autobrr/internal/metrics/collector"
|
||||
"github.com/autobrr/autobrr/internal/release"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
)
|
||||
|
||||
type MetricsManager struct {
|
||||
registry *prometheus.Registry
|
||||
}
|
||||
|
||||
func NewMetricsManager(version string, commit string, date string, releaseService release.Service, ircService irc.Service, feedService feed.Service, listService list.Service, filterService filter.Service) *MetricsManager {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(
|
||||
collectors.NewGoCollector(),
|
||||
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
||||
prometheus.NewGaugeFunc(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "autobrr_info",
|
||||
Help: "Autobrr version information",
|
||||
ConstLabels: prometheus.Labels{
|
||||
"version": version,
|
||||
"build_time": date,
|
||||
"revision": commit,
|
||||
},
|
||||
},
|
||||
func() float64 { return 1 },
|
||||
),
|
||||
collector.NewReleaseCollector(releaseService),
|
||||
collector.NewIRCCollector(ircService),
|
||||
collector.NewFeedCollector(feedService),
|
||||
collector.NewListCollector(listService),
|
||||
collector.NewFilterCollector(filterService),
|
||||
)
|
||||
return &MetricsManager{
|
||||
registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MetricsManager) GetRegistry() *prometheus.Registry {
|
||||
return s.registry
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue