main.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // example-grpc-producer demonstrates how a peer Go service publishes
  2. // alerts to ingestd via the gRPC StreamAlerts bidi-stream.
  3. //
  4. // Usage:
  5. //
  6. // BA_API_KEY="company:source:secret" \
  7. // go run ./cmd/example-grpc-producer \
  8. // --addr localhost:9090 \
  9. // --rate 100 \
  10. // --duration 30s
  11. //
  12. // This is the reference implementation for the "How to publish to broad-announce"
  13. // story documented in SPEC §19.
  14. package main
  15. import (
  16. "context"
  17. "flag"
  18. "fmt"
  19. "log/slog"
  20. "math/rand"
  21. "os"
  22. "sync/atomic"
  23. "time"
  24. grpcclient "git3.techno-world.net/lrosales/broad-announce/internal/grpcclient"
  25. pbv1 "git3.techno-world.net/lrosales/broad-announce/gen/go/broadannounce/v1"
  26. )
  27. func main() {
  28. addr := flag.String("addr", "localhost:9090", "ingestd gRPC address")
  29. rate := flag.Int("rate", 100, "alerts per second")
  30. duration := flag.Duration("duration", 30*time.Second, "how long to run")
  31. flag.Parse()
  32. apiKey := os.Getenv("BA_API_KEY")
  33. if apiKey == "" {
  34. fmt.Fprintln(os.Stderr, "BA_API_KEY env var is required")
  35. os.Exit(1)
  36. }
  37. logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
  38. Level: slog.LevelInfo,
  39. }))
  40. ctx, cancel := context.WithTimeout(context.Background(), *duration)
  41. defer cancel()
  42. // Connect to ingestd.
  43. c, err := grpcclient.New(*addr,
  44. grpcclient.WithAPIKey(apiKey),
  45. grpcclient.WithInsecure(), // remove in production (use TLS/mTLS)
  46. )
  47. if err != nil {
  48. logger.Error("grpcclient.New", "addr", *addr, "err", err)
  49. os.Exit(1)
  50. }
  51. defer c.Close()
  52. // Open a stream.
  53. stream, err := c.Stream(ctx)
  54. if err != nil {
  55. logger.Error("Client.Stream", "err", err)
  56. os.Exit(1)
  57. }
  58. defer stream.CloseSend()
  59. var sent int64
  60. // Producer goroutine: generates alerts at --rate.
  61. alertCh := make(chan *pbv1.Alert, *rate*2)
  62. go func() {
  63. ticker := time.NewTicker(time.Second / time.Duration(*rate))
  64. defer ticker.Stop()
  65. defer close(alertCh)
  66. i := 0
  67. for {
  68. select {
  69. case <-ctx.Done():
  70. return
  71. case <-ticker.C:
  72. alertCh <- randomAlert(i)
  73. i++
  74. }
  75. }
  76. }()
  77. // Send loop: pulls from alertCh and sends to ingestd.
  78. go func() {
  79. for alert := range alertCh {
  80. if err := stream.Send(ctx, alert); err != nil {
  81. logger.Warn("stream.Send", "err", err)
  82. continue
  83. }
  84. atomic.AddInt64(&sent, 1)
  85. if sent%100 == 0 {
  86. logger.Info("sent", "count", sent)
  87. }
  88. }
  89. }()
  90. // Recv loop: reads Acks and logs every 100.
  91. for {
  92. ack, err := stream.Recv(ctx)
  93. if err != nil {
  94. if ctx.Err() != nil {
  95. return // normal shutdown
  96. }
  97. logger.Error("stream.Recv", "err", err)
  98. return
  99. }
  100. if atomic.LoadInt64(&sent)%100 == 0 {
  101. logger.Info("ack", "dedupe_key", ack.DedupeKey)
  102. }
  103. }
  104. }
  105. func randomAlert(i int) *pbv1.Alert {
  106. severities := []string{"critical", "warning", "info"}
  107. return &pbv1.Alert{
  108. CompanyId: "acme-001",
  109. SourceId: "prom-prod",
  110. Severity: severities[rand.Intn(len(severities))],
  111. Category: "monitoring",
  112. Title: fmt.Sprintf("example alert %d", i),
  113. Body: "This is a demo alert from example-grpc-producer.",
  114. DedupeKey: fmt.Sprintf("example-%d", i),
  115. ClientTsMs: time.Now().UnixMilli(),
  116. Data: map[string]string{
  117. "host": fmt.Sprintf("host-%d", rand.Intn(10)),
  118. "value": fmt.Sprintf("%d", rand.Intn(100)),
  119. },
  120. }
  121. }