diff --git a/docs/config.md b/docs/config.md index 6c299621..4bcf3683 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1099,7 +1099,7 @@ If a non-200 HTTP status code is returned or if the returned `health` field is ` See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment. -## Metrics +## Monitoring If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)). @@ -1108,7 +1108,7 @@ listen address. Metrics may be considered sensitive information, so before you e doing, and/or secure access to the endpoint in your reverse proxy. - `enable-metrics` enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket) -- `metrics-listen-http` exposes the metrics endpoint via a dedicated [IP]:port. If set, this option implicitly +- `metrics-listen-http` exposes the metrics endpoint via a dedicated `[IP]:port`. If set, this option implicitly enables metrics as well, e.g. "10.0.1.1:9090" or ":9090" === Using default port diff --git a/server/server.go b/server/server.go index 230c8e4d..b6d3c4cf 100644 --- a/server/server.go +++ b/server/server.go @@ -618,6 +618,7 @@ func (s *Server) handleMatrixDiscovery(w http.ResponseWriter) error { } func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, error) { + start := time.Now() t, err := fromContext[*topic](r, contextTopic) if err != nil { return nil, err @@ -707,6 +708,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if unifiedpush { minc(metricUnifiedPushPublishedSuccess) } + mset(metricMessagePublishDurationMillis, time.Since(start).Milliseconds()) return m, nil } diff --git a/server/server_manager.go b/server/server_manager.go index 0d9bb2c6..891366f7 100644 --- a/server/server_manager.go +++ b/server/server_manager.go @@ -63,6 +63,15 @@ func (s *Server) execManager() { sentMailTotal, sentMailSuccess, sentMailFailure = s.smtpSender.Counts() } + // Users + var usersCount int64 + if s.userManager != nil { + usersCount, err = s.userManager.UsersCount() + if err != nil { + log.Tag(tagManager).Err(err).Warn("Error counting users") + } + } + // Print stats s.mu.Lock() messagesCount, topicsCount, visitorsCount := s.messages, len(s.topics), len(s.visitors) @@ -75,6 +84,7 @@ func (s *Server) execManager() { "topics_active": topicsCount, "subscribers": subscribers, "visitors": visitorsCount, + "users": usersCount, "emails_received": receivedMailTotal, "emails_received_success": receivedMailSuccess, "emails_received_failure": receivedMailFailure, @@ -85,6 +95,7 @@ func (s *Server) execManager() { Info("Server stats") mset(metricMessagesCached, messagesCached) mset(metricVisitors, visitorsCount) + mset(metricUsers, usersCount) mset(metricSubscribers, subscribers) mset(metricTopics, topicsCount) } diff --git a/server/server_metrics.go b/server/server_metrics.go index 560bf2eb..d3f17929 100644 --- a/server/server_metrics.go +++ b/server/server_metrics.go @@ -5,23 +5,25 @@ import ( ) var ( - metricMessagesPublishedSuccess prometheus.Counter - metricMessagesPublishedFailure prometheus.Counter - metricMessagesCached prometheus.Gauge - metricFirebasePublishedSuccess prometheus.Counter - metricFirebasePublishedFailure prometheus.Counter - metricEmailsPublishedSuccess prometheus.Counter - metricEmailsPublishedFailure prometheus.Counter - metricEmailsReceivedSuccess prometheus.Counter - metricEmailsReceivedFailure prometheus.Counter - metricUnifiedPushPublishedSuccess prometheus.Counter - metricMatrixPublishedSuccess prometheus.Counter - metricMatrixPublishedFailure prometheus.Counter - metricAttachmentsTotalSize prometheus.Gauge - metricVisitors prometheus.Gauge - metricSubscribers prometheus.Gauge - metricTopics prometheus.Gauge - metricHTTPRequests *prometheus.CounterVec + metricMessagesPublishedSuccess prometheus.Counter + metricMessagesPublishedFailure prometheus.Counter + metricMessagesCached prometheus.Gauge + metricMessagePublishDurationMillis prometheus.Gauge + metricFirebasePublishedSuccess prometheus.Counter + metricFirebasePublishedFailure prometheus.Counter + metricEmailsPublishedSuccess prometheus.Counter + metricEmailsPublishedFailure prometheus.Counter + metricEmailsReceivedSuccess prometheus.Counter + metricEmailsReceivedFailure prometheus.Counter + metricUnifiedPushPublishedSuccess prometheus.Counter + metricMatrixPublishedSuccess prometheus.Counter + metricMatrixPublishedFailure prometheus.Counter + metricAttachmentsTotalSize prometheus.Gauge + metricVisitors prometheus.Gauge + metricSubscribers prometheus.Gauge + metricTopics prometheus.Gauge + metricUsers prometheus.Gauge + metricHTTPRequests *prometheus.CounterVec ) func initMetrics() { @@ -34,6 +36,9 @@ func initMetrics() { metricMessagesCached = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ntfy_messages_cached_total", }) + metricMessagePublishDurationMillis = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "ntfy_message_publish_duration_ms", + }) metricFirebasePublishedSuccess = prometheus.NewCounter(prometheus.CounterOpts{ Name: "ntfy_firebase_published_success", }) @@ -67,6 +72,9 @@ func initMetrics() { metricVisitors = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ntfy_visitors_total", }) + metricUsers = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "ntfy_users_total", + }) metricSubscribers = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "ntfy_subscribers_total", }) @@ -80,6 +88,7 @@ func initMetrics() { metricMessagesPublishedSuccess, metricMessagesPublishedFailure, metricMessagesCached, + metricMessagePublishDurationMillis, metricFirebasePublishedSuccess, metricFirebasePublishedFailure, metricEmailsPublishedSuccess, @@ -91,6 +100,7 @@ func initMetrics() { metricMatrixPublishedFailure, metricAttachmentsTotalSize, metricVisitors, + metricUsers, metricSubscribers, metricTopics, metricHTTPRequests, diff --git a/user/manager.go b/user/manager.go index 7fe115d9..b2898ae8 100644 --- a/user/manager.go +++ b/user/manager.go @@ -169,6 +169,7 @@ const ( ELSE 2 END, user ` + selectUserCountQuery = `SELECT COUNT(*) FROM user` updateUserPassQuery = `UPDATE user SET pass = ? WHERE user = ?` updateUserRoleQuery = `UPDATE user SET role = ? WHERE user = ?` updateUserPrefsQuery = `UPDATE user SET prefs = ? WHERE id = ?` @@ -853,6 +854,23 @@ func (a *Manager) Users() ([]*User, error) { return users, nil } +// UsersCount returns the number of users in the databsae +func (a *Manager) UsersCount() (int64, error) { + rows, err := a.db.Query(selectUserCountQuery) + if err != nil { + return 0, err + } + defer rows.Close() + if !rows.Next() { + return 0, errNoRows + } + var count int64 + if err := rows.Scan(&count); err != nil { + return 0, err + } + return count, nil +} + // User returns the user with the given username if it exists, or ErrUserNotFound otherwise. // You may also pass Everyone to retrieve the anonymous user and its Grant list. func (a *Manager) User(username string) (*User, error) {