snapshot of https://github.com/ThreeDotsLabs/watermill.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
master / ci (push) Failing after 0s

This commit is contained in:
upstream-mirror
2026-09-15 21:27:46 +05:00
commit ce933d1277
538 changed files with 46504 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
validation_cmd: "docker compose up"
teardown_cmd: "docker compose down"
timeout: 180
expected_output: "msg=\"Message acked\""

View 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:
![Grafana login screen](https://threedots.tech/watermill-io/grafana_login.png)
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`.
![Prometheus data source configuration](https://threedots.tech/watermill-io/prometheus_data_source_config.png)
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.
![Importing the dashboard](https://threedots.tech/watermill-io/grafana_import_dashboard.png)
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).

View 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

View 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

View 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=

View 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...)
}

View 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']