Batched event delivery for Go services
Patrol buffers the events your service reports and flushes them in batches to Slack, Discord, SMS, email, a SQL table or a server of your own. 100 events or 5s, whichever comes first. Patrol Cloud runs that server for you and keeps the history.
package main
import (
"context"
"fmt"
"log"
"github.com/kataras/patrol"
)
func main() {
ctx := context.Background()
slack, err := patrol.NewSlack(patrol.SlackOptions{
Token: "xoxb-...",
Channels: []string{"C0123456789"},
})
if err != nil {
log.Fatal(err)
}
producer := patrol.NewProducer(patrol.ProducerOptions{
ProjectName: "My Project",
OnError: func(err error) { log.Println("patrol:", err) },
}, slack)
defer producer.Close(ctx)
e := patrol.NewException(fmt.Errorf("payment failed")).
WithField("UserEmail", "user@example.com").
WithMentions("U0123456789")
if err := producer.SendEvent(ctx, e); err != nil {
log.Fatal(err)
}
}- Batched deliveryEvents are buffered and flushed when the batch reaches MaxBatchSize or BufferInterval elapses, 100 events or 5 seconds by default.
- Independent flushersA failing destination never blocks or cancels the others, and OnError receives one joined error naming each flusher that failed.
- Graceful shutdownClose(ctx) drains the buffer and honours its deadline: when the context ends first it cancels the in-flight flush and returns ctx.Err().
- Stacktraces that open in your editorNewException captures a filtered call stack, drops the runtime and patrol frames, and renders each one as a vscode://file link.
How it works
Three steps, one goroutine
Patrol adds one background loop to your process. Nothing else changes about how your code reports what happened.
SendEvent buffers
Your code calls producer.SendEvent(ctx, event) and gets back immediately. The event goes into a channel, not onto the network. Debug events are dropped here while debug mode is off, so leaving them in costs one comparison.
The batch flushes
A background loop flushes when the batch reaches MaxBatchSize or when BufferInterval elapses, 100 events or 5s out of the box. The batch is sorted once by capture time and handed to the flushers read-only.
Every flusher runs
All destinations run concurrently and each one runs to completion. A failure in one is collected, not propagated: the errors are joined, prefixed with the flusher name, and passed to your OnError callback.
On the way out, producer.Close(ctx) drains what is buffered and honours its deadline. When the context ends first it cancels the in-flight flush, returnsctx.Err() and finishes the shutdown in the background.
Destinations
Seven places a batch can land
Each one is a constructor with an options struct carrying json, yaml and toml tags, so it loads straight from your config file. Pass as many as you want to NewProducer.
Slack
patrol.NewSlackOne Block Kit message per channel and project, with mentions and file uploads.
Discord
patrol.NewDiscordOne message per project group, posted by a bot to the channels you list.
Twilio
patrol.NewTwilioSMS, or WhatsApp when both numbers carry the whatsapp: prefix.
Email over SMTP
patrol.NewInboxAn HTML email per project group, sent over SMTP with STARTTLS when the server offers it.
SQL
patrol.NewSQLRows in a patrol_events table, 1000 to a statement, in one transaction.
HTTP client
patrol.NewClientPosts the batch as JSON to a patrol Server, or to Patrol Cloud, with a bearer token or a username and password.
HTTP server
patrol.NewServerAn http.Handler that receives those batches and hands them to its own flushers.
Patrol Cloud
Every event, with the stack that caused it
The console stores what your producer sent and shows it back: the message, the fields you attached, the mentions, and the filtered call stack with a link that opens the frame in your editor.
Latest events
- error05:41:07
- info05:40:49
- error05:40:20
- info05:39:31
- debug05:38:55
- error05:37:42
Event detail
Fields
- OrderID
- ord_7Kq3R9
- Amount
- 4290
- Currency
- EUR
- Attempt
- 2
- Retryable
- false
Stacktrace
- mainmain.go:61
- HandlerFunc.ServeHTTPserver.go:2294
- (*Handler).Payhandler.go:148
- (*Charger).Chargecharger.go:92
Frames marked in your own module are highlighted. In Slack, Discord and email the same frames are links of the form vscode://file/path:line.
Self-host or Cloud
One wire format, two places to point it
A Client posts a JSON array of events to POST / and checks the connection withGET /. Bodies over 32 MiB are refused with 413. Both ends of that contract are in the library, so moving between them is a BaseURL and a credential.
Run the server yourself
patrol.NewServer is an http.Handler. Give it the flushers you want, guard it with a bearer token or a username and password, and mount it wherever you already run HTTP. Nothing leaves your network.
srv := patrol.NewServer(patrol.ServerOptions{
Tokens: []string{os.Getenv("PATROL_TOKEN")},
OnError: func(err error) { log.Println(err) },
}, slack, inbox)
log.Fatal(http.ListenAndServe(":8080", srv))Let Patrol Cloud run it
The same Client, pointed at https://api.patrol.hellenic.dev with one key from the console. Events are stored per project, kept for as long as your plan says, and fanned out to your Slack, Discord, email and Twilio destinations from there.
client := patrol.NewClient(patrol.ClientOptions{
BaseURL: "https://api.patrol.hellenic.dev",
Token: os.Getenv("PATROL_KEY"),
})
producer := patrol.NewProducer(patrol.ProducerOptions{ProjectName: "checkout-api"}, client)Patrol Cloud pricing
Start on the free plan
The library is free under BSD-3-Clause whatever you choose here. These plans are for the hosted side: storage, the console, keys and the fan-out.
Free
$0per month
One service, one alert channel, no card.
- Events
- 10,000 a month
- History
- 7 days
- Projects
- 2
- Destinations
- 2 per project
- Batch
- 4 MiB
Pro
Pricing coming soon
A production service with a team watching it.
- Events
- 500,000 a month
- History
- 30 days
- Projects
- 10
- Destinations
- 10 per project
- Batch
- 16 MiB
Business
Pricing coming soon
Many services, longer history, more people.
- Events
- 5,000,000 a month
- History
- 90 days
- Projects
- 50
- Destinations
- 25 per project
- Batch
- 32 MiB
FAQ
Questions people ask first
What is Patrol?
- Patrol is a Go library that collects the events your service reports and delivers them in batches. You call SendEvent, the producer buffers, and a background loop flushes the batch to every destination you configured: Slack, Discord, SMS, email, a SQL table or another patrol server. Patrol Cloud is the hosted half. The same library posts to api.patrol.hellenic.dev instead, and a console shows the events with their fields and stacktraces. One import, one package, and no agent process to run beside your own.
Which destinations can Patrol deliver to?
- Seven: Slack, Discord, Twilio for SMS or WhatsApp, email over SMTP, a SQL database, an HTTP client that posts to a remote patrol server, and that server itself. Each one is a constructor with an options struct carrying json, yaml and toml tags, so it loads straight from a config file. Any type with a FlushEvents(ctx, evts) error method is a destination too, which is how a queue, a webhook or a file gets added without waiting for a release.
Can I self-host instead of using Patrol Cloud?
- Yes, and it is the same code either way. Run patrol.NewServer in a service of your own, give it the flushers you want, and point patrol.NewClient at it from everywhere else. Patrol Cloud is that server operated for you, with storage, a console, per-project keys and quotas on top. The wire format does not change: POST / with a JSON array of events, GET / for a connectivity check, Basic auth or one bearer token. Moving between the two is a BaseURL and a credential.
Is the library free and open source?
- The library is BSD-3-Clause on GitHub and stays that way. Install it with go get github.com/kataras/patrol@latest and use it in a commercial product with no fee and no key. Patrol Cloud is the paid part, and its free plan covers 10,000 events a month with 7 days of history. Nothing in the library phones home and nothing in it is gated behind an account: a service with a Slack flusher and no Patrol Cloud project keeps working exactly as it did.
What happens when one destination fails?
- The others still deliver. Every flusher in a batch runs concurrently and to completion, so a Slack outage does not cancel the SQL write and a wrong SMTP password does not stop the Discord message. The failures are joined into one error, each prefixed with the flusher name, and handed to the OnError callback on ProducerOptions. The library retries nothing and writes nothing to disk: a destination that needs retries wraps itself. On Patrol Cloud the failure is recorded per destination and shown in the console.
Does Patrol only work with Iris?
- No. Patrol is a plain Go module with no framework dependency. It behaves the same in a net/http service, a gRPC server, a background worker, a CLI or a test: create one producer at startup, call SendEvent where something happens, call Close on shutdown. The question comes up because the same author wrote the Iris web framework and the two are often used together. Nothing in patrol imports Iris, and nothing in it assumes an HTTP request is in flight.
Which Go version does Patrol need?
- Go 1.27 or newer. Version 0.0.6 moved to the standard library uuid and encoding/json/v2 packages and to testing/synctest for its own tests, dropping github.com/google/uuid and golang.org/x/sync on the way. The one remaining dependency is github.com/kataras/basicauth, which the HTTP server uses for constant-time credential checks. Older Go releases are not supported: the module declares 1.27 and the compiler says so before anything runs.
How do the stacktraces work?
- patrol.NewException(err) captures the call stack where it is created, then drops the frames nobody reads: the runtime, the testing package, the frames from patrol itself, and any module prefix passed to AddSkipModuleFrames. What is left is outermost first, each frame carrying the function, the module, the file, the line and an in_app flag. Templates render them as vscode://file/path:line links, so a frame in a Slack message or an email opens in your editor on the right line.
Add it in one line
Go 1.27 or newer, BSD-3-Clause, one package and one dependency. The hosted console is free to try on 10,000 events a month.
go get github.com/kataras/patrol@latest