snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s
Some checks failed
master / ci (push) Failing after 0s
This commit is contained in:
4
_examples/basic/4-metrics/.validate_example.yml
Normal file
4
_examples/basic/4-metrics/.validate_example.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
validation_cmd: "docker compose up"
|
||||
teardown_cmd: "docker compose down"
|
||||
timeout: 180
|
||||
expected_output: "msg=\"Message acked\""
|
||||
74
_examples/basic/4-metrics/README.md
Normal file
74
_examples/basic/4-metrics/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Prometheus metrics showcase
|
||||
|
||||
This is an example application that showcases how Watermill may be monitored with Prometheus metrics.
|
||||
|
||||
The docker-compose bundle contains the following services:
|
||||
|
||||
#### Golang
|
||||
|
||||
A [Golang](https://hub.docker.com/_/golang) image which runs the [example code](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/4-metrics/main.go). It consists of a router with a single handler.
|
||||
|
||||
The handler consumes messages from a [Gochannel PubSub](https://github.com/ThreeDotsLabs/watermill/tree/master/message/infrastructure/gochannel), and publishes 0-4 copies of the message with a preconfigured random delay.
|
||||
|
||||
Additionally, there is one goroutine which produces messages incoming to the handler with a gochannel publisher, and another goroutine which consumes the messages outgoing from the handler.
|
||||
|
||||
The router, the standalone publisher and the standalone subscriber are all decorated with the metrics code and their statistics will appear in the dashboard.
|
||||
|
||||
#### Prometheus
|
||||
[Prometheus](https://hub.docker.com/r/prom/prometheus/), to scrape the metrics which the golang application exposes at `:8081/metrics` by default. It is configured by a [prometheus.yml](https://github.com/ThreeDotsLabs/watermill/blob/master/_examples/basic/4-metrics/prometheus.yml) file, which declares the endpoints that Prometheus will scrape.
|
||||
|
||||
#### Grafana
|
||||
[Grafana](https://hub.docker.com/r/grafana/grafana), to visualize the metrics in a dashboard.
|
||||
|
||||
#### Running the example
|
||||
|
||||
To run the docker-compose bundle, go to `_examples/metrics` and execute:
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
The golang app will start running, producing messages, passing them through the handler, and consuming the copies.
|
||||
|
||||
With default settings, the raw Prometheus metrics should appear at your http://localhost:8081/metrics.
|
||||
|
||||
The Prometheus image should expose a more advanced UI at http://localhost:9090/graph, where you can investigate all the scraped metrics.
|
||||
|
||||
However, what is the most useful way to monitor is through the use of Grafana, which you can access at http://localhost:3000.
|
||||
|
||||
#### Adding the Prometheus data source to Grafana
|
||||
|
||||
The fresh Grafana image should greet you with a login screen:
|
||||
|
||||

|
||||
|
||||
Just use the default `admin:admin` credentials. You can skip changing the password, if you wish.
|
||||
|
||||
The next thing that we need to do is to add the Prometheus data source. Click on `Add data source`.
|
||||
|
||||
In the following screen:
|
||||
|
||||
1. Enter a name for the Prometheus data source. Let's name this data source `prometheus`.
|
||||
1. Choose `Prometheus` from the `Type` dropdown.
|
||||
1. Enter the `http://localhost:9090` value in the HTTP/URL section.
|
||||
1. You can leave the remaining settings at default and click `Save & Test`.
|
||||
|
||||

|
||||
|
||||
The Prometheus data source is now ready to use in Grafana.
|
||||
|
||||
#### Importing the Grafana dashboard
|
||||
|
||||
We have prepared a Grafana dashboard that visualizes the metrics exported by this example.
|
||||
|
||||
To import the Grafana dashboard, select Dashboard/Manage from the left menu, and then click on `+Import` (or go to http://localhost:3000/dashboard/import).
|
||||
|
||||
Enter the dashboard URL https://grafana.com/dashboards/9777 (or just the ID, 9777), and click on Load.
|
||||
|
||||

|
||||
|
||||
Then select the Prometheus data source created in the previous step. Click on `Import`, and you're done!
|
||||
|
||||
### Find out more
|
||||
|
||||
To find out more, about metrics be sure to check out the [Watermill docs](https://watermill.io/docs/metrics).
|
||||
26
_examples/basic/4-metrics/docker-compose.yml
Normal file
26
_examples/basic/4-metrics/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
golang:
|
||||
image: golang:1.26
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8080:8080
|
||||
- 8081:8081
|
||||
depends_on:
|
||||
- prometheus
|
||||
volumes:
|
||||
- ../../../:/watermill
|
||||
- $GOPATH/pkg/mod:/go/pkg/mod
|
||||
working_dir: /watermill/_examples/basic/4-metrics
|
||||
command: go run main.go -metrics :8081 -delay 0.1
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:5.2.4
|
||||
network_mode: host
|
||||
|
||||
28
_examples/basic/4-metrics/go.mod
Normal file
28
_examples/basic/4-metrics/go.mod
Normal file
@@ -0,0 +1,28 @@
|
||||
module main.go
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/sony/gobreaker v1.0.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
// uncomment to use local sources
|
||||
// replace github.com/ThreeDotsLabs/watermill => ../../../../watermill
|
||||
56
_examples/basic/4-metrics/go.sum
Normal file
56
_examples/basic/4-metrics/go.sum
Normal file
@@ -0,0 +1,56 @@
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
|
||||
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/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
146
_examples/basic/4-metrics/main.go
Normal file
146
_examples/basic/4-metrics/main.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill/components/metrics"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/middleware"
|
||||
"github.com/ThreeDotsLabs/watermill/message/router/plugin"
|
||||
"github.com/ThreeDotsLabs/watermill/pubsub/gochannel"
|
||||
)
|
||||
|
||||
var (
|
||||
metricsAddr = flag.String("metrics", ":8081", "The address that will expose /metrics for Prometheus")
|
||||
handlerDelay = flag.Float64("delay", 0, "The stdev of normal distribution of delay in handler (in seconds), to simulate load")
|
||||
|
||||
logger = watermill.NewStdLogger(true, true)
|
||||
random = rand.New(rand.NewSource(time.Now().Unix()))
|
||||
)
|
||||
|
||||
func delay() {
|
||||
seconds := *handlerDelay
|
||||
if seconds == 0 {
|
||||
return
|
||||
}
|
||||
delay := math.Abs(random.NormFloat64() * seconds)
|
||||
time.Sleep(time.Duration(float64(time.Second) * delay))
|
||||
}
|
||||
|
||||
// handler publishes 0–3 messages after a random delay.
|
||||
func handler(msg *message.Message) ([]*message.Message, error) {
|
||||
delay()
|
||||
|
||||
numOutgoing := random.Intn(4)
|
||||
outgoing := make([]*message.Message, numOutgoing)
|
||||
|
||||
for i := 0; i < numOutgoing; i++ {
|
||||
outgoing[i] = msg.Copy()
|
||||
}
|
||||
return outgoing, nil
|
||||
}
|
||||
|
||||
// consumeMessages consumes messages published by the handler.
|
||||
func consumeMessages(subscriber message.Subscriber) {
|
||||
messages, err := subscriber.Subscribe(context.Background(), "pub_topic")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for msg := range messages {
|
||||
msg.Ack()
|
||||
}
|
||||
}
|
||||
|
||||
// produceMessages generates incoming messages with delays of 50–100 milliseconds.
|
||||
func produceMessages(routerClosed chan struct{}, publisher message.Publisher) {
|
||||
for {
|
||||
select {
|
||||
case <-routerClosed:
|
||||
return
|
||||
default:
|
||||
// go on
|
||||
}
|
||||
|
||||
time.Sleep(50*time.Millisecond + time.Duration(random.Intn(50))*time.Millisecond)
|
||||
msg := message.NewMessage(watermill.NewUUID(), []byte{})
|
||||
_ = publisher.Publish("sub_topic", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
pubSub := gochannel.NewGoChannel(gochannel.Config{}, logger)
|
||||
|
||||
router, err := message.NewRouter(
|
||||
message.RouterConfig{},
|
||||
logger,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
prometheusRegistry, closeMetricsServer := metrics.CreateRegistryAndServeHTTP(*metricsAddr)
|
||||
defer closeMetricsServer()
|
||||
|
||||
// we leave the namespace and subsystem empty
|
||||
metricsBuilder := metrics.NewPrometheusMetricsBuilder(prometheusRegistry, "", "")
|
||||
metricsBuilder.AddPrometheusRouterMetrics(router)
|
||||
|
||||
router.AddMiddleware(
|
||||
middleware.Recoverer,
|
||||
middleware.RandomFail(0.1),
|
||||
middleware.RandomPanic(0.1),
|
||||
)
|
||||
router.AddPlugin(plugin.SignalsHandler)
|
||||
|
||||
router.AddHandler(
|
||||
"metrics-example",
|
||||
"sub_topic",
|
||||
pubSub,
|
||||
"pub_topic",
|
||||
pubSub,
|
||||
handler,
|
||||
)
|
||||
|
||||
pub := randomFailPublisherDecorator{pubSub, 0.1}
|
||||
|
||||
// The handler's publisher and subscriber will be decorated by `AddPrometheusRouterMetrics`.
|
||||
// We are using the same pub/sub to generate messages incoming to the handler
|
||||
// and consume the outgoing messages.
|
||||
// These will have the label handler_name=<no handler> in Prometheus.
|
||||
subWithMetrics, err := metricsBuilder.DecorateSubscriber(pubSub)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pubWithMetrics, err := metricsBuilder.DecoratePublisher(pub)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
routerClosed := make(chan struct{})
|
||||
go produceMessages(routerClosed, pubWithMetrics)
|
||||
go consumeMessages(subWithMetrics)
|
||||
|
||||
_ = router.Run(context.Background())
|
||||
close(routerClosed)
|
||||
}
|
||||
|
||||
type randomFailPublisherDecorator struct {
|
||||
message.Publisher
|
||||
failProbability float64
|
||||
}
|
||||
|
||||
func (r randomFailPublisherDecorator) Publish(topic string, messages ...*message.Message) error {
|
||||
if random.Float64() < r.failProbability {
|
||||
return errors.New("random publishing failure")
|
||||
}
|
||||
return r.Publisher.Publish(topic, messages...)
|
||||
}
|
||||
20
_examples/basic/4-metrics/prometheus.yml
Normal file
20
_examples/basic/4-metrics/prometheus.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# my global config
|
||||
global:
|
||||
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
|
||||
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
|
||||
# scrape_timeout is set to the global default (10s).
|
||||
|
||||
scrape_configs:
|
||||
# # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
|
||||
# - job_name: 'prometheus'
|
||||
#
|
||||
# # metrics_path defaults to '/metrics'
|
||||
# # scheme defaults to 'http'.
|
||||
#
|
||||
# static_configs:
|
||||
# - targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'metrics_example'
|
||||
|
||||
static_configs:
|
||||
- targets: ['localhost:8081']
|
||||
Reference in New Issue
Block a user