Skip to content
Patrol

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.

  1. 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.

  2. 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.

  3. 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.

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.

checkout-apiEvents05:41:07 UTC

Latest events

  • errorcharge declined: card_declined05:41:07
  • infoorder paid05:40:49
  • errorcontext deadline exceeded calling inventory05:40:20
  • inforefund issued05:39:31
  • debugidempotency key reused, returning the stored response05:38:55
  • errorwebhook signature mismatch05:37:42

Event detail

charge declined: card_declined

checkout-api · captured 05:41:07 UTC · id019943b0

Fields

OrderID
ord_7Kq3R9
Amount
4290
Currency
EUR
Attempt
2
Retryable
false

Stacktrace

  1. mainmain.go:61
  2. HandlerFunc.ServeHTTPserver.go:2294
  3. (*Handler).Payhandler.go:148
  4. (*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))
Read the self-hosting guide

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)
Open the console

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
    Start free
  • 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
    Open the console

Compare plans feature by feature

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