diff --git a/go.mod b/go.mod index bab564939..b41d4e994 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,8 @@ require ( github.com/tencent-connect/botgo v0.2.1 github.com/traefik/yaegi v0.16.1 go.mau.fi/whatsmeow v0.0.0-20260219150138-7ae702b1eed4 + go.opentelemetry.io/otel v1.29.0 + go.opentelemetry.io/otel/metric v1.29.0 golang.org/x/oauth2 v0.35.0 golang.org/x/time v0.14.0 google.golang.org/protobuf v1.36.11 @@ -49,6 +51,8 @@ require ( github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/go-jose/go-jose/v3 v3.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -68,6 +72,7 @@ require ( github.com/vektah/gqlparser/v2 v2.5.27 // indirect go.mau.fi/libsignal v0.2.1 // indirect go.mau.fi/util v0.9.6 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect diff --git a/pkg/health/resource_tracker.go b/pkg/health/resource_tracker.go index 8ac247814..96f063a7b 100644 --- a/pkg/health/resource_tracker.go +++ b/pkg/health/resource_tracker.go @@ -6,6 +6,8 @@ import ( "sync" "time" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" "jane/pkg/logger" ) @@ -16,6 +18,12 @@ type ResourceTracker struct { interval time.Duration stopCh chan struct{} stopOnce sync.Once + + // OpenTelemetry gauges + goroutinesGauge metric.Int64ObservableGauge + allocMBGauge metric.Float64ObservableGauge + totalMBGauge metric.Float64ObservableGauge + sysMBGauge metric.Float64ObservableGauge } // NewResourceTracker creates a new ResourceTracker that logs metrics every `interval`. @@ -23,10 +31,77 @@ func NewResourceTracker(interval time.Duration) *ResourceTracker { if interval == 0 { interval = 60 * time.Second // Default to 1 minute } - return &ResourceTracker{ + + rt := &ResourceTracker{ interval: interval, stopCh: make(chan struct{}), } + + meter := otel.Meter("jane/pkg/health") + + var err error + rt.goroutinesGauge, err = meter.Int64ObservableGauge( + "sys.goroutines", + metric.WithDescription("Number of active goroutines"), + ) + if err != nil { + logger.ErrorCF("SystemHealth", "Failed to create goroutines gauge", map[string]any{"error": err.Error()}) + } + + rt.allocMBGauge, err = meter.Float64ObservableGauge( + "sys.memory.alloc_mb", + metric.WithDescription("Memory allocated and still in use, in MB"), + ) + if err != nil { + logger.ErrorCF("SystemHealth", "Failed to create memory alloc gauge", map[string]any{"error": err.Error()}) + } + + rt.totalMBGauge, err = meter.Float64ObservableGauge( + "sys.memory.total_alloc_mb", + metric.WithDescription("Total memory allocated (even if freed), in MB"), + ) + if err != nil { + logger.ErrorCF("SystemHealth", "Failed to create total memory alloc gauge", map[string]any{"error": err.Error()}) + } + + rt.sysMBGauge, err = meter.Float64ObservableGauge( + "sys.memory.sys_mb", + metric.WithDescription("Total memory obtained from the OS, in MB"), + ) + if err != nil { + logger.ErrorCF("SystemHealth", "Failed to create memory sys gauge", map[string]any{"error": err.Error()}) + } + + if _, err := meter.RegisterCallback(rt.observeMetrics, rt.goroutinesGauge, rt.allocMBGauge, rt.totalMBGauge, rt.sysMBGauge); err != nil { + logger.ErrorCF("SystemHealth", "Failed to register metrics callback", map[string]any{"error": err.Error()}) + } + + return rt +} + +func (rt *ResourceTracker) observeMetrics(_ context.Context, o metric.Observer) error { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + goroutines := runtime.NumGoroutine() + allocMB := float64(m.Alloc) / 1024 / 1024 + totalAllocMB := float64(m.TotalAlloc) / 1024 / 1024 + sysMB := float64(m.Sys) / 1024 / 1024 + + if rt.goroutinesGauge != nil { + o.ObserveInt64(rt.goroutinesGauge, int64(goroutines)) + } + if rt.allocMBGauge != nil { + o.ObserveFloat64(rt.allocMBGauge, allocMB) + } + if rt.totalMBGauge != nil { + o.ObserveFloat64(rt.totalMBGauge, totalAllocMB) + } + if rt.sysMBGauge != nil { + o.ObserveFloat64(rt.sysMBGauge, sysMB) + } + + return nil } // Start begins tracking resources in a background goroutine.