package metrics import ( "fmt" "friendica-exporter/internal/client" "friendica-exporter/serverinfo" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "strconv" ) const ( metricPrefix = "friendica_" labelErrorCauseOther = "other" labelErrorCauseAuth = "auth" labelErrorCauseRateLimit = "ratelimit" ) var ( updateAvailableDesc = prometheus.NewDesc( metricPrefix+"update_available", "Contains information whether a system update is available (0 = no, 1 = yes). The available_version level contains the latest available Friendica version, whereas the version level contains the current installed Friendica version.", []string{"version", "available_version"}, nil) updateStatus = prometheus.NewDesc( metricPrefix+"update_status", "Contains information whether a system update was successful (1 = no, 0 = yes).", nil, nil) updateDatabaseStatus = prometheus.NewDesc( metricPrefix+"update_db_status", "Contains information whether a database update was successful (0 = no, 1 = yes).", nil, nil) cronLastExecution = prometheus.NewDesc( metricPrefix+"cron_last_execution", "Contains information about the last execution of cron of this Instance.", []string{"datetime"}, nil) workerLastExecution = prometheus.NewDesc( metricPrefix+"worker_last_execution", "Contains information about the last execution of worker of this Instance.", []string{"datetime"}, nil) workerJPM = prometheus.NewDesc( metricPrefix+"worker_jpm", "Number of jobs per Minute.", []string{"frequency"}, nil) workerTasksTotal = prometheus.NewDesc( metricPrefix+"worker_tasks_total", "Number of worker tasks of this Instance.", []string{"level"}, nil) workerTasksActive = prometheus.NewDesc( metricPrefix+"worker_tasks_active", "Number of active worker tasks of this Instance.", []string{"level"}, nil) usersTotal = prometheus.NewDesc( metricPrefix+"users_total", "Number of total users of this Instance.", nil, nil) usersActiveWeek = prometheus.NewDesc( metricPrefix+"users_active_week", "Number of active users of the last week.", nil, nil) usersActiveMonth = prometheus.NewDesc( metricPrefix+"users_active_month", "Number of active users of the last month.", nil, nil) usersActiveHalfYear = prometheus.NewDesc( metricPrefix+"users_active_half_year", "Number of active users of the last half year.", nil, nil) postsInbound = prometheus.NewDesc( metricPrefix+"posts_inbound", "Number of posts inbound.", []string{"type"}, nil) postsOutbound = prometheus.NewDesc( metricPrefix+"posts_outbound", "Number of posts outbound.", []string{"type"}, nil) packetsInbound = prometheus.NewDesc( metricPrefix+"packets_inbound", "Number of packets inbound.", []string{"protocol"}, nil) packetsOutbound = prometheus.NewDesc( metricPrefix+"packets_outbound", "Number of packets outbound.", []string{"protocol"}, nil) usersPending = prometheus.NewDesc( metricPrefix+"users_pending", "Number of pending users.", nil, nil) reportsNewest = prometheus.NewDesc( metricPrefix+"reports_newest", "Contains the datetime about the newest report of this Instance.", []string{"datetime"}, nil) reportsOpen = prometheus.NewDesc( metricPrefix+"reports_open", "Number of open reports.", nil, nil) reportsClosed = prometheus.NewDesc( metricPrefix+"reports_closed", "Number of closed reports.", nil, nil) serverInfoDesc = prometheus.NewDesc( metricPrefix+"server_info", "Contains meta information about Server as labels. Value is always 1.", []string{"version"}, nil) phpInfoDesc = prometheus.NewDesc( metricPrefix+"php_info", "Contains meta information about PHP as labels. Value is always 1.", []string{"version"}, nil) phpMemoryLimitDesc = prometheus.NewDesc( metricPrefix+"php_memory_limit_bytes", "Configured PHP memory limit in bytes.", nil, nil) phpMaxUploadSizeDesc = prometheus.NewDesc( metricPrefix+"php_upload_max_size_bytes", "Configured maximum upload size in bytes.", nil, nil) phpMaxPostSizeDesc = prometheus.NewDesc( metricPrefix+"php_post_max_size_bytes", "Configured maximum post size in bytes.", nil, nil) databaseMaxAllowedPacketDesc = prometheus.NewDesc( metricPrefix+"php_post_max_allowed_packet", "Configured maximum allowed packet length to send to or receive from the server.", nil, nil) ) type friendicaCollector struct { log logrus.FieldLogger infoClient client.InfoClient upMetric prometheus.Gauge scrapeErrorsMetric *prometheus.CounterVec } func RegisterCollector(log logrus.FieldLogger, infoClient client.InfoClient) error { c := &friendicaCollector{ log: log, infoClient: infoClient, upMetric: prometheus.NewGauge(prometheus.GaugeOpts{ Name: metricPrefix + "up", Help: "Indicates if the metrics could be scraped by the exporter.", }), scrapeErrorsMetric: prometheus.NewCounterVec(prometheus.CounterOpts{ Name: metricPrefix + "scrape_errors_total", Help: "Counts the number of scrape errors by this collector.", }, []string{"cause"}), } return prometheus.Register(c) } func (c *friendicaCollector) Describe(ch chan<- *prometheus.Desc) { c.upMetric.Describe(ch) c.scrapeErrorsMetric.Describe(ch) ch <- updateAvailableDesc ch <- updateStatus ch <- updateDatabaseStatus ch <- cronLastExecution ch <- workerLastExecution ch <- workerJPM ch <- workerTasksTotal ch <- workerTasksActive ch <- usersTotal ch <- usersActiveWeek ch <- usersActiveMonth ch <- usersActiveHalfYear ch <- usersPending ch <- postsInbound ch <- postsOutbound ch <- packetsInbound ch <- packetsOutbound ch <- reportsNewest ch <- reportsOpen ch <- reportsClosed ch <- serverInfoDesc ch <- phpInfoDesc ch <- phpMaxUploadSizeDesc ch <- phpMaxPostSizeDesc ch <- phpMemoryLimitDesc ch <- databaseMaxAllowedPacketDesc } func (c *friendicaCollector) Collect(ch chan<- prometheus.Metric) { if err := c.collectFriendica(ch); err != nil { c.log.Errorf("Error during scrape: %s", err) cause := labelErrorCauseOther if err == client.ErrNotAuthorized { cause = labelErrorCauseAuth } else if err == client.ErrRatelimit { cause = labelErrorCauseRateLimit } c.scrapeErrorsMetric.WithLabelValues(cause).Inc() c.upMetric.Set(0) } else { c.upMetric.Set(1) } c.upMetric.Collect(ch) c.scrapeErrorsMetric.Collect(ch) } func (c *friendicaCollector) collectFriendica(ch chan<- prometheus.Metric) error { status, err := c.infoClient() if err != nil { return err } return readMetrics(ch, status) } func readMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { if err := collectSimpleMetrics(ch, status); err != nil { return err } if err := collectWorkerTotal(ch, status); err != nil { return err } if err := collectJPM(ch, status); err != nil { return err } if err := collectLastExecutions(ch, status); err != nil { return err } if err := collectUpdate(ch, status); err != nil { return err } if err := collectPosts(ch, status); err != nil { return err } if err := collectPacketsPerDirection(ch, status, packetsInbound); err != nil { return err } if err := collectPacketsPerDirection(ch, status, packetsOutbound); err != nil { return err } if err := collectPhpMetrics(ch, status); err != nil { return err } return nil } func collectUpdate(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { updateInfo := status.Update serverInfo := status.Server updateAvailableValue := 0.0 // Fix small bug: its indicated as "true" even if there is no real update available. if updateInfo.Available && serverInfo.Version != updateInfo.AvailableVersion { updateAvailableValue = 1.0 } metric, err := prometheus.NewConstMetric(updateAvailableDesc, prometheus.GaugeValue, updateAvailableValue, serverInfo.Version, updateInfo.AvailableVersion) if err != nil { return fmt.Errorf("error creating metric for %s: %w", updateAvailableDesc, err) } ch <- metric return nil } type simpleMetric struct { desc *prometheus.Desc value float64 } func collectSimpleMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { metrics := []simpleMetric{ { desc: updateStatus, value: float64(status.Update.Status), }, { desc: updateDatabaseStatus, value: float64(status.Update.DatabaseStatus), }, { desc: usersTotal, value: float64(status.Users.Total), }, { desc: usersActiveWeek, value: float64(status.Users.ActiveWeek), }, { desc: usersActiveMonth, value: float64(status.Users.ActiveMonth), }, { desc: usersActiveHalfYear, value: float64(status.Users.ActiveHalfYear), }, { desc: usersPending, value: float64(status.Users.Pending), }, { desc: reportsOpen, value: float64(status.Reports.Open), }, { desc: reportsClosed, value: float64(status.Reports.Closed), }, } for _, m := range metrics { metric, err := prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, m.value) if err != nil { return fmt.Errorf("error creating metric for %s: %w", m.desc, err) } ch <- metric } return nil } type workerMetric struct { desc *prometheus.Desc value float64 level string } func collectWorkerTotal(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { metrics := []workerMetric{ { desc: workerTasksActive, value: float64(status.Worker.Active.Critical), level: "critical", }, { desc: workerTasksActive, value: float64(status.Worker.Active.High), level: "high", }, { desc: workerTasksActive, value: float64(status.Worker.Active.Medium), level: "medium", }, { desc: workerTasksActive, value: float64(status.Worker.Active.Low), level: "low", }, { desc: workerTasksActive, value: float64(status.Worker.Active.Negligible), level: "negligible", }, { desc: workerTasksTotal, value: float64(status.Worker.Total.Critical), level: "critical", }, { desc: workerTasksTotal, value: float64(status.Worker.Total.High), level: "high", }, { desc: workerTasksTotal, value: float64(status.Worker.Total.Medium), level: "medium", }, { desc: workerTasksTotal, value: float64(status.Worker.Total.Low), level: "low", }, { desc: workerTasksTotal, value: float64(status.Worker.Total.Negligible), level: "negligible", }, } for _, m := range metrics { metric, err := prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, m.value, m.level) if err != nil { return fmt.Errorf("error creating metric for %s: %w", m.desc, err) } ch <- metric } return nil } type jpmMetric struct { desc *prometheus.Desc value float64 frequency string } func collectJPM(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { metrics := []jpmMetric{ { desc: workerJPM, value: float64(status.Worker.JPM.OneMinute), frequency: "1 minute", }, { desc: workerJPM, value: float64(status.Worker.JPM.ThreeMinutes), frequency: "3 minutes", }, { desc: workerJPM, value: float64(status.Worker.JPM.FiveMinutes), frequency: "5 minutes", }, } for _, m := range metrics { metric, err := prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, m.value, m.frequency) if err != nil { return fmt.Errorf("error creating metric for %s: %w", m.desc, err) } ch <- metric } return nil } func collectLastExecutions(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { workerInfo := status.Worker metric, err := prometheus.NewConstMetric(workerLastExecution, prometheus.GaugeValue, float64(workerInfo.LastExecution.Timestamp), workerInfo.LastExecution.DateTime) if err != nil { return fmt.Errorf("error creating metric for %s: %w", workerLastExecution, err) } ch <- metric cronInfo := status.Cron metric, err = prometheus.NewConstMetric(cronLastExecution, prometheus.GaugeValue, float64(cronInfo.LastExecution.Timestamp), cronInfo.LastExecution.DateTime) if err != nil { return fmt.Errorf("error creating metric for %s: %w", cronLastExecution, err) } ch <- metric reportsInfo := status.Reports metric, err = prometheus.NewConstMetric(reportsNewest, prometheus.GaugeValue, float64(reportsInfo.Newest.Timestamp), reportsInfo.Newest.DateTime) if err != nil { return fmt.Errorf("error creating metric for %s: %w", reportsNewest, err) } ch <- metric return nil } type postMetric struct { desc *prometheus.Desc value float64 postType string } func collectPosts(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { metrics := []postMetric{ { desc: postsInbound, value: float64(status.Posts.Inbound.Posts), postType: "posts", }, { desc: postsInbound, value: float64(status.Posts.Inbound.Comments), postType: "comments", }, { desc: postsOutbound, value: float64(status.Posts.Outbound.Posts), postType: "posts", }, { desc: postsOutbound, value: float64(status.Posts.Outbound.Comments), postType: "comments", }, } for _, m := range metrics { metric, err := prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, m.value, m.postType) if err != nil { return fmt.Errorf("error creating metric for %s: %w", m.desc, err) } ch <- metric } return nil } type packetMetrics struct { desc *prometheus.Desc value float64 protocol string } func collectPacketsPerDirection(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo, desc *prometheus.Desc) error { var packetInfo serverinfo.PacketCounts if desc == packetsInbound { packetInfo = status.Packets.Inbound } else { packetInfo = status.Packets.Outbound } metrics := []packetMetrics{ { desc: desc, value: float64(packetInfo.ActivityPub), protocol: "apub", }, { desc: desc, value: float64(packetInfo.DFRN), protocol: "dfrn", }, { desc: desc, value: float64(packetInfo.Feed), protocol: "feed", }, { desc: desc, value: float64(packetInfo.Diaspora), protocol: "dspr", }, { desc: desc, value: float64(packetInfo.Mail), protocol: "mail", }, { desc: desc, value: float64(packetInfo.OStatus), protocol: "stat", }, { desc: desc, value: float64(packetInfo.Bluesky), protocol: "bsky", }, { desc: desc, value: float64(packetInfo.Thumblr), protocol: "tmbl", }, } for _, m := range metrics { metric, err := prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, m.value, m.protocol) if err != nil { return fmt.Errorf("error creating metric for %s: %w", m.desc, err) } ch <- metric } return nil } func collectPhpMetrics(ch chan<- prometheus.Metric, status *serverinfo.ServerInfo) error { metric, err := prometheus.NewConstMetric(phpInfoDesc, prometheus.GaugeValue, 1, status.Server.PHP.Version) if err != nil { return fmt.Errorf("error creating metric for %s: %w", phpInfoDesc, err) } ch <- metric metric, err = prometheus.NewConstMetric(serverInfoDesc, prometheus.GaugeValue, 1, status.Server.Version) if err != nil { return fmt.Errorf("error creating metric for %s: %w", serverInfoDesc, err) } ch <- metric metricVal, err := strconv.ParseFloat(status.Server.Database.MaxAllowedPacket, 64) if err != nil { return fmt.Errorf("error converting value to byte for %s: %w", databaseMaxAllowedPacketDesc, err) } metric, err = prometheus.NewConstMetric(databaseMaxAllowedPacketDesc, prometheus.GaugeValue, metricVal) if err != nil { return fmt.Errorf("error creating metric for %s: %w", databaseMaxAllowedPacketDesc, err) } ch <- metric metrics := []struct { desc *prometheus.Desc value string }{ { desc: phpMemoryLimitDesc, value: status.Server.PHP.MemoryLimit, }, { desc: phpMaxPostSizeDesc, value: status.Server.PHP.PostMaxSize, }, { desc: phpMaxUploadSizeDesc, value: status.Server.PHP.UploadMaxFilesize, }, } for _, m := range metrics { metricVal, err := ConvertMemoryToBytes(m.value) if err != nil { return fmt.Errorf("error converting value to byte for %s: %w", m.desc, err) } metric, err = prometheus.NewConstMetric(m.desc, prometheus.GaugeValue, float64(metricVal)) if err != nil { return fmt.Errorf("error creating metric for %s: %w", m.desc, err) } ch <- metric } return nil }