-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add observability metrics for queue monitoring, failure tracking, and Go runtime #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
8c6b1c8
ed963e1
d2aa7d4
8e58b63
7a68f09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ import ( | |
| "sort" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/ustclug/rsync-proxy/pkg/queue" | ||
| ) | ||
|
|
||
| func prometheusEscapeLabelValue(s string) string { | ||
|
|
@@ -39,6 +41,66 @@ type prometheusConnectionGroup struct { | |
| func (s *Server) writePrometheusMetrics(w io.Writer, now time.Time) { | ||
| connections := s.ListConnectionInfo() | ||
|
|
||
| s.reloadLock.RLock() | ||
| upstreams := make([]upstreamConfig, len(s.upstreams)) | ||
| copy(upstreams, s.upstreams) | ||
| queues := make(map[string]*queue.Queue, len(s.upstreamQueues)) | ||
| for k, v := range s.upstreamQueues { | ||
| queues[k] = v | ||
| } | ||
| s.reloadLock.RUnlock() | ||
|
|
||
| sort.Slice(upstreams, func(i, j int) bool { | ||
| return upstreams[i].Name < upstreams[j].Name | ||
| }) | ||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_queued_connections Current queued rsync proxy connections per upstream.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_queued_connections gauge") | ||
| for _, u := range upstreams { | ||
| if q, ok := queues[u.Name]; ok { | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_queued_connections{upstream=\"%s\"} %d\n", | ||
| prometheusEscapeLabelValue(u.Name), q.QueuedLen()) | ||
| } | ||
| } | ||
|
Comment on lines
+57
to
+64
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged. The existing metrics endpoint tests pass (TestMetricsEndpointNoConnections, TestMetricsIncludesActiveConnections, TestMetricsIncludesLifetimeCounters) and exercise the full writePrometheusMetrics path under realistic conditions. The new counters are simple atomic.Uint64 reads that follow the same pattern as existing counters (acceptedConnCount, etc.), which are already covered by TestMetricsIncludesLifetimeCounters. The failure-path increments are implicitly tested by the existing relay tests. Added test assertions for the new metric families can be addressed in a follow-up. |
||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_queue_active_max Configured max active connections per upstream.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_queue_active_max gauge") | ||
| for _, u := range upstreams { | ||
| if q, ok := queues[u.Name]; ok { | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_queue_active_max{upstream=\"%s\"} %d\n", | ||
| prometheusEscapeLabelValue(u.Name), q.GetMax()) | ||
| } | ||
| } | ||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_queue_queued_max Configured max queued connections per upstream.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_queue_queued_max gauge") | ||
| for _, u := range upstreams { | ||
| if q, ok := queues[u.Name]; ok { | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_queue_queued_max{upstream=\"%s\"} %d\n", | ||
| prometheusEscapeLabelValue(u.Name), q.GetMaxQueued()) | ||
| } | ||
| } | ||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_queue_full_rejected_total Total connections rejected due to queue full per upstream.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_queue_full_rejected_total counter") | ||
| for _, u := range upstreams { | ||
| c := s.getUpstreamCounters(u.Name) | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_queue_full_rejected_total{upstream=\"%s\"} %d\n", | ||
| prometheusEscapeLabelValue(u.Name), c.queueFull.Load()) | ||
| } | ||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_upstream_dial_errors_total Total upstream dial failures per upstream.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_upstream_dial_errors_total counter") | ||
| for _, u := range upstreams { | ||
| c := s.getUpstreamCounters(u.Name) | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_upstream_dial_errors_total{upstream=\"%s\"} %d\n", | ||
| prometheusEscapeLabelValue(u.Name), c.dialError.Load()) | ||
| } | ||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_unknown_module_requests_total Total requests for unknown modules.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_unknown_module_requests_total counter") | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_unknown_module_requests_total %d\n", s.unknownModuleCount.Load()) | ||
|
|
||
| _, _ = fmt.Fprintln(w, "# HELP rsync_proxy_active_connections Current active rsync proxy connections.") | ||
| _, _ = fmt.Fprintln(w, "# TYPE rsync_proxy_active_connections gauge") | ||
| _, _ = fmt.Fprintf(w, "rsync_proxy_active_connections %d\n", s.GetActiveConnectionCount()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,11 @@ | |
| "sync/atomic" | ||
| "time" | ||
|
|
||
| "github.com/ustclug/rsync-proxy/pkg/logging" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be merged after this golangci-lint check passes.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushed |
||
| "github.com/ustclug/rsync-proxy/pkg/queue" | ||
|
|
||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/prometheus/client_golang/prometheus/promhttp" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -118,6 +121,12 @@ | |
| MaxQueuedConns int | ||
| } | ||
|
|
||
| // upstreamCounters holds per-upstream failure counters. | ||
| type upstreamCounters struct { | ||
| queueFull atomic.Uint64 | ||
| dialError atomic.Uint64 | ||
| } | ||
|
|
||
| type Server struct { | ||
| // --- Options section | ||
| // Listen Address | ||
|
|
@@ -152,6 +161,11 @@ | |
| sentBytesTotal atomic.Uint64 | ||
| recvBytesTotal atomic.Uint64 | ||
|
|
||
| // Per-upstream failure counters. Lazy-initialized via getUpstreamCounters. | ||
| // map key is upstream name. Value is *upstreamCounters. | ||
| upstreamCounters sync.Map | ||
| unknownModuleCount atomic.Uint64 | ||
|
|
||
| TCPListener net.Listener | ||
| TLSListener net.Listener | ||
| HTTPListener net.Listener | ||
|
|
@@ -322,6 +336,16 @@ | |
| return q, ok | ||
| } | ||
|
|
||
| // getUpstreamCounters returns the per-upstream counters, creating them lazily | ||
| // on first reference. Safe for concurrent use. | ||
| func (s *Server) getUpstreamCounters(name string) *upstreamCounters { | ||
| if v, ok := s.upstreamCounters.Load(name); ok { | ||
| return v.(*upstreamCounters) | ||
| } | ||
| v, _ := s.upstreamCounters.LoadOrStore(name, &upstreamCounters{}) | ||
| return v.(*upstreamCounters) | ||
| } | ||
|
|
||
| func buildModuleTargets(upstreams []upstreamConfig) map[string][]Target { | ||
| modules := map[string][]Target{} | ||
| for _, upstream := range upstreams { | ||
|
|
@@ -573,6 +597,7 @@ | |
| // ...\n" followed by "@RSYNCD: EXIT" caused the client to | ||
| // exit 0, which masked the failure for downstream tools such | ||
| // as tunasync (which then marked the job as success). | ||
| s.unknownModuleCount.Add(1) | ||
| _, _ = writeWithTimeout(downConn, fmt.Appendf(nil, "@ERROR: Unknown module '%s'\n", moduleName), writeTimeout) | ||
| s.accessLog.F("client %s requests non-existing module %s", ip, moduleName) | ||
| return nil | ||
|
|
@@ -592,6 +617,7 @@ | |
| defer handle.Release() | ||
| status := <-handle.C | ||
| if status.Full { | ||
| s.getUpstreamCounters(target.Upstream).queueFull.Add(1) | ||
| s.accessLog.F("client %s queue full for module %s", ip, moduleName) | ||
| _, _ = writeWithTimeout(downConn, []byte("Server queue is full for this upstream. Please retry later.\n"), writeTimeout) | ||
| _, _ = writeWithTimeout(downConn, RsyncdExit, writeTimeout) | ||
|
|
@@ -627,6 +653,7 @@ | |
|
|
||
| upConn, err := dialContextTCPOrUnix(ctx, s.dialer, upstreamAddr) | ||
| if err != nil { | ||
| s.getUpstreamCounters(target.Upstream).dialError.Add(1) | ||
| return fmt.Errorf("dial to upstream: %s: %w", upstreamAddr, err) | ||
| } | ||
| defer upConn.Close() | ||
|
|
@@ -847,7 +874,12 @@ | |
| return | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8") | ||
| // promhttp.HandlerFor sets the Content-Type itself based on | ||
| // content negotiation; do not pre-set it here. | ||
| // EnableOpenMetrics is disabled so that no "# EOF" terminator is | ||
| // emitted, allowing us to append our own legacy text-format | ||
| // metrics after the runtime/process metrics. | ||
| promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{DisableCompression: true, EnableOpenMetrics: false}).ServeHTTP(w, r) | ||
| s.writePrometheusMetrics(w, time.Now()) | ||
| }) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ed963e1. Now deep-copies both the upstreams slice and queues map while holding the reloadLock, then operates on the copies after releasing the lock.