diff --git a/bot/config/config.go b/bot/config/config.go index 07e8139f..36de1f15 100644 --- a/bot/config/config.go +++ b/bot/config/config.go @@ -32,6 +32,9 @@ type Config struct { BranchLookup VCS `mapstructure:"branch_lookup"` + // Metrics, like Prometheus + Metrics Metrics `mapstructure:"metrics"` + OpenWeather OpenWeather `mapstructure:"open_weather"` PullRequest PullRequest `mapstructure:"pullrequest"` Timezone string `mapstructure:"timezone"` diff --git a/bot/config/metrics.go b/bot/config/metrics.go new file mode 100644 index 00000000..801caa5c --- /dev/null +++ b/bot/config/metrics.go @@ -0,0 +1,6 @@ +package config + +type Metrics struct { + // e.g. use ":8082" to expose metrics on all interfaces + PrometheusListener string `mapstructure:"prometheus_listener"` +} diff --git a/bot/listener.go b/bot/listener.go index d0528d0c..4d12bc4c 100644 --- a/bot/listener.go +++ b/bot/listener.go @@ -24,6 +24,7 @@ func (b *Bot) startRunnables(ctx *util.ServerContext) { // Run is blocking method to handle new incoming events...from different sources func (b *Bot) Run(ctx *util.ServerContext) { b.startRunnables(ctx) + initMetrics(b.config, ctx) // initialize Socket Mode: // https://api.slack.com/apis/connections/socket diff --git a/bot/metrics.go b/bot/metrics.go new file mode 100644 index 00000000..20c5a7c1 --- /dev/null +++ b/bot/metrics.go @@ -0,0 +1,73 @@ +package bot + +import ( + "net/http" + "strings" + "time" + + "github.com/innogames/slack-bot/v2/bot/config" + "github.com/innogames/slack-bot/v2/bot/stats" + "github.com/innogames/slack-bot/v2/bot/util" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + log "github.com/sirupsen/logrus" +) + +type statRegistry struct{} + +// Describe returns all descriptions of the collector. +func (c *statRegistry) Describe(_ chan<- *prometheus.Desc) { + // unused in our simple case... +} + +// Collect returns the current state of all metrics of our slack-bot stats +func (c *statRegistry) Collect(ch chan<- prometheus.Metric) { + for _, key := range stats.GetKeys() { + metric := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "slack_bot", + Name: strings.ReplaceAll(key, "-", "_"), + }) + value, _ := stats.Get(key) + metric.Set(float64(value)) + metric.Collect(ch) + } +} + +func initMetrics(cfg config.Config, ctx *util.ServerContext) { + if cfg.Metrics.PrometheusListener == "" { + return + } + + registry := prometheus.NewRegistry() + registry.MustRegister( + &statRegistry{}, + collectors.NewGoCollector(), + ) + + go func() { + ctx.RegisterChild() + defer ctx.ChildDone() + + log.Infof("Init prometheus handler on http://%s/metrics", cfg.Metrics.PrometheusListener) + + server := &http.Server{ + Addr: cfg.Metrics.PrometheusListener, + ReadHeaderTimeout: 3 * time.Second, + } + + http.Handle( + "/metrics", promhttp.HandlerFor( + registry, + promhttp.HandlerOpts{}, + ), + ) + + go func() { + _ = server.ListenAndServe() + }() + + <-ctx.Done() + _ = server.Shutdown(ctx) + }() +} diff --git a/bot/metrics_test.go b/bot/metrics_test.go new file mode 100644 index 00000000..56ebb22e --- /dev/null +++ b/bot/metrics_test.go @@ -0,0 +1,52 @@ +package bot + +import ( + "io" + "net" + "net/http" + "testing" + "time" + + "github.com/innogames/slack-bot/v2/bot/config" + "github.com/innogames/slack-bot/v2/bot/stats" + "github.com/innogames/slack-bot/v2/bot/util" + + "github.com/stretchr/testify/assert" +) + +func TestMetrics(t *testing.T) { + ctx := util.NewServerContext() + defer ctx.StopTheWorld() + + metricsPort := getPort() + + cfg := config.Config{ + Metrics: config.Metrics{ + PrometheusListener: metricsPort, + }, + } + + stats.Set("test_value", 500) + + initMetrics(cfg, ctx) + time.Sleep(time.Millisecond * 100) + + resp, err := http.Get("http://" + metricsPort + "/metrics") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + assert.Equal(t, 200, resp.StatusCode) + + content, _ := io.ReadAll(resp.Body) + assert.Contains(t, string(content), "slack_bot_test_value 500") +} + +// get a random free port on the host +func getPort() string { + l, _ := net.Listen("tcp4", "localhost:0") + defer l.Close() + + return l.Addr().String() +} diff --git a/command/cron/cron_test.go b/command/cron/cron_test.go index 44a401ff..d4f5262f 100644 --- a/command/cron/cron_test.go +++ b/command/cron/cron_test.go @@ -3,6 +3,7 @@ package cron import ( "strings" "testing" + "time" "github.com/innogames/slack-bot/v2/bot" "github.com/innogames/slack-bot/v2/bot/config" @@ -53,6 +54,7 @@ func TestCron(t *testing.T) { t.Run("Run in background", func(t *testing.T) { ctx := util.NewServerContext() go command.RunAsync(ctx) + time.Sleep(time.Millisecond * 10) ctx.StopTheWorld() }) diff --git a/go.mod b/go.mod index 41e02488..1a9a93e4 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/gookit/color v1.5.4 github.com/hackebrot/turtle v0.2.0 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.17.0 github.com/redis/go-redis/v9 v9.2.1 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/robfig/cron/v3 v3.0.1 @@ -30,6 +31,7 @@ require ( require ( github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect @@ -46,9 +48,13 @@ require ( github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -64,7 +70,6 @@ require ( golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e3da92f3..fd8e3d97 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/aws/aws-sdk-go v1.45.26 h1:PJ2NJNY5N/yeobLYe1Y+xLdavBi67ZI8gvph6ftwVC github.com/aws/aws-sdk-go v1.45.26/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bndr/gojenkins v1.1.0 h1:TWyJI6ST1qDAfH33DQb3G4mD8KkrBfyfSUoZBHQAvPI= @@ -255,7 +257,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -272,6 +273,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -302,13 +305,21 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= +github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= @@ -318,7 +329,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sdomino/golang-scribble v0.0.0-20230717151034-b95d4df19aa8 h1:JRRoBVPTQF96yoE0NmIpQN4Si7Fb+Ls8Llw57BeBwwI= @@ -737,7 +748,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=