# Patrol: batched event delivery for Go services Patrol is a Go library that collects the events a service reports and delivers them in batches to the places a team already watches. A Producer buffers what your code sends, a background loop flushes the batch when it is full or when the interval elapses, and every configured destination receives it concurrently. Patrol Cloud is the hosted half: the same library posts to a server we run, which stores the events, shows them in a console and fans them out to your alert channels. This file is the long reference for https://patrol.hellenic.dev. The short index is at https://patrol.hellenic.dev/llms.txt. ## Identity - Name: Patrol - Go module: github.com/kataras/patrol - Install: go get github.com/kataras/patrol@latest - Minimum Go version: 1.27 - License: BSD-3-Clause - Source: https://github.com/kataras/patrol - API reference: https://pkg.go.dev/github.com/kataras/patrol - Newest version: v0.0.8 (not tagged yet) - Website: https://patrol.hellenic.dev/ - Documentation: https://patrol.hellenic.dev/docs/ - Console: https://patrol.hellenic.dev/app/ - Hosted ingest: https://api.patrol.hellenic.dev - Publisher: Hellenic Development, https://hellenic.dev ## What the library does **Batched delivery.** Events are buffered and flushed when the batch reaches MaxBatchSize or BufferInterval elapses, 100 events or 5 seconds by default. **Independent flushers.** A failing destination never blocks or cancels the others, and OnError receives one joined error naming each flusher that failed. **Graceful shutdown.** Close(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 editor.** NewException captures a filtered call stack, drops the runtime and patrol frames, and renders each one as a vscode://file link. ## How a batch travels Your code calls `producer.SendEvent(ctx, event)` and returns immediately. The event goes into a buffered channel rather than onto the network, so the calling goroutine is not waiting on Slack. Debug events are dropped at this point while debug mode is off, which is why leaving them in the code costs one comparison. A background loop owns the buffer. It flushes when the batch reaches MaxBatchSize or when BufferInterval elapses, 100 events or 5s when the options are left at zero. The batch is sorted once by capture time and then handed to every flusher as a read-only slice, because they run at the same time and share it. Flushers are independent by design. Each one runs to completion, a failure in one never cancels another, and the errors are joined into a single error whose parts are prefixed with the flusher names before being passed to the OnError callback on ProducerOptions. The library retries nothing and persists nothing: a destination that needs retries wraps itself. Shutdown is `producer.Close(ctx)`. It waits for any debugging session, flushes what is buffered, closes the flushers that hold resources, and honours the context deadline. When the deadline passes first it cancels the in-flight flush, returns `ctx.Err()` and finishes in the background. Close is idempotent, and SendEvent returns ErrClosed afterwards. ## Destinations Every destination is a constructor taking an options struct whose fields carry `json`, `yaml` and `toml` tags, so the same struct loads from any of the three config formats. Constructors validate their required fields and return an error before building a client. ### Slack One Block Kit message per channel and project, with mentions and file uploads. Constructor: `patrol.NewSlack`. Required options: Token, Channels. ### Discord One message per project group, posted by a bot to the channels you list. Constructor: `patrol.NewDiscord`. Required options: BotToken, Channels. ### Twilio SMS, or WhatsApp when both numbers carry the whatsapp: prefix. Constructor: `patrol.NewTwilio`. Required options: AccountSID, AuthToken, FromNumber, ToNumber. ### Email over SMTP An HTML email per project group, sent over SMTP with STARTTLS when the server offers it. Constructor: `patrol.NewInbox`. Required options: From, To, Host, Port. ### SQL Rows in a patrol_events table, 1000 to a statement, in one transaction. Constructor: `patrol.NewSQL`. Required options: Engine. ### HTTP client Posts the batch as JSON to a patrol Server, or to Patrol Cloud, with a bearer token or a username and password. Constructor: `patrol.NewClient`. No option is required. ### HTTP server An http.Handler that receives those batches and hands them to its own flushers. Constructor: `patrol.NewServer`. No option is required. ### Writing your own Any type with a `FlushEvents(ctx context.Context, evts []*patrol.Event) error` method is an EventFlusher, and `patrol.EventFlusherFunc` adapts a plain function. The batch is sorted and shared, so a flusher must not sort, append to or mutate it. A flusher may also implement `Test(ctx) error`, `Close(ctx) error` and `String() string`; the name from String is what appears in error messages. ## The HTTP protocol A patrol Server is an `http.Handler`. It accepts `POST /` with a JSON array of events and answers `GET /` with the literal body `OK` for connectivity checks. Any other method gets 405 with an `Allow` header. Errors are a JSON object with `http_error_code` and `message`, and the codes are exported constants: METHOD_NOT_ALLOWED, UNAUTHENTICATED, INVALID_ARGUMENT, PAYLOAD_TOO_LARGE, RESOURCE_EXHAUSTED and UNAVAILABLE. Authentication is HTTP Basic through `ServerOptions.BasicAuth`, a bearer token from `ServerOptions.Tokens`, or both, in which case either credential is accepted. A client sends one with `ClientOptions.BasicAuth` or `ClientOptions.Token`. A request body over 32 MiB (`patrol.MaxRequestBody`) is refused with 413 and PAYLOAD_TOO_LARGE before it is decoded, so a sender can tell "shrink the batch" from "bad JSON". The server answers as soon as the batch is decoded and delivers on a context detached from the request, so delivery outlives the response. ## Patrol Cloud Patrol Cloud is that server operated for you at https://api.patrol.hellenic.dev. An unmodified `patrol.Client` talks to it: set BaseURL to the ingest host and Token to the key the console gives you. Events are stored per project, shown in a console with their fields and stacktraces, and fanned out to the Slack, Discord, email and Twilio destinations you configure there instead of in your code. Sign-in is GitHub. An ingest key is a public id plus a secret shown once, stored as a SHA-256 hash and compared in constant time. Going over a plan limit does not drop events on the spot: they keep being stored and marked for 24 hours, after which ingest answers 429 with RESOURCE_EXHAUSTED. ### Plans **Free ($0).** One service, one alert channel, no card. 10,000 events a month, 7 days of history, 2 projects, 2 destinations per project, batches up to 4 MiB. Includes: Sign in with GitHub; Slack, Discord, email and Twilio destinations; Stacktraces with editor deep links; Ingest keys you can revoke. **Pro (Pricing coming soon).** A production service with a team watching it. 500,000 events a month, 30 days of history, 10 projects, 10 destinations per project, batches up to 16 MiB. Includes: Everything in Free; One rule per destination, filtered by event type; Delivery failures kept per destination; Email support from the people who wrote the library. **Business (Pricing coming soon).** Many services, longer history, more people. 5,000,000 events a month, 90 days of history, 50 projects, 25 destinations per project, batches up to 32 MiB. Includes: Everything in Pro; 90 days of event history; Batches up to the 32 MiB the library allows; A named contact for incidents. Pro and Business have no price yet. The site shows that plainly rather than a placeholder number. ## Privacy and security The library opens no listening socket, reads and writes no file, and sends nothing anywhere it was not configured to send. There is no telemetry and no update check. What an event contains is what your code puts in it. Patrol Cloud stores the event as sent, the account, the projects, hashed ingest keys, the destinations and the delivery results. Credentials given to a destination are encrypted at rest with AES-256-GCM and are never returned by the API. Events are deleted when the plan's retention window passes. Sub-processors are GitHub, for hosting this site and for sign-in, and Stripe, for payment. This website carries no analytics and no tracking cookie. Report a vulnerability to contact@hellenic.dev. Report a library bug at https://github.com/kataras/patrol/issues. ## Documentation map ### Getting started - Overview: https://patrol.hellenic.dev/docs/ - Install: https://patrol.hellenic.dev/docs/getting-started/install/ - Your first event: https://patrol.hellenic.dev/docs/getting-started/first-event/ ### Producer and events - Producer and batching: https://patrol.hellenic.dev/docs/producer/ - Events: https://patrol.hellenic.dev/docs/producer/events/ - Debug events: https://patrol.hellenic.dev/docs/producer/debug-events/ - Shutdown: https://patrol.hellenic.dev/docs/producer/shutdown/ - Stacktraces: https://patrol.hellenic.dev/docs/producer/stacktraces/ ### Integrations - Slack: https://patrol.hellenic.dev/docs/integrations/slack/ - Discord: https://patrol.hellenic.dev/docs/integrations/discord/ - Twilio: https://patrol.hellenic.dev/docs/integrations/twilio/ - Email: https://patrol.hellenic.dev/docs/integrations/email/ - SQL: https://patrol.hellenic.dev/docs/integrations/sql/ - HTTP client: https://patrol.hellenic.dev/docs/integrations/http-client/ ### Self-hosting a Server - The Server: https://patrol.hellenic.dev/docs/self-hosting/ - Deploy: https://patrol.hellenic.dev/docs/self-hosting/deploy/ ### Patrol Cloud - Overview: https://patrol.hellenic.dev/docs/cloud/ - Projects: https://patrol.hellenic.dev/docs/cloud/projects/ - Ingest keys: https://patrol.hellenic.dev/docs/cloud/ingest-keys/ - Destinations: https://patrol.hellenic.dev/docs/cloud/destinations/ - Rules: https://patrol.hellenic.dev/docs/cloud/rules/ - Limits: https://patrol.hellenic.dev/docs/cloud/limits/ ### Custom flushers - Write a flusher: https://patrol.hellenic.dev/docs/custom-flushers/ ### Configuration file keys - Every option key: https://patrol.hellenic.dev/docs/configuration/ ### Reference - Links and examples: https://patrol.hellenic.dev/docs/reference/ - Changelog: https://patrol.hellenic.dev/changelog/ ## Release notes ### v0.0.8 (unreleased) Shutting a `Server` down properly, and the last piece a custom server needed. **Added** - `Server.Close(ctx)` stops accepting batches, waits for the deliveries already in flight and then closes every flusher that implements `Closer`. A `Server` answers a POST before it delivers, so `http.Server.Shutdown` returning told you nothing about whether the events arrived, and a flusher holding a resource, a `SQL` pool for instance, was never closed at all. When `ctx` ends first it returns `ctx.Err()` and leaves the deliveries running, without closing the flushers: closing one underneath a delivery is worse than leaking it. It is idempotent, and afterwards a POST is refused with 503 and `UNAVAILABLE` while GET still answers `OK`, so a health check can watch the drain. - `SortEvents(evts)`, the order `Producer` and `Server` put a batch in before handing it to the flushers. A program that runs its own server needs it to satisfy `FlushAll`'s contract, and was reimplementing it. ### v0.0.7 Everything a program needs to run its own server that speaks the `Server` protocol, plus a single-token credential. Additive: code written against v0.0.6 keeps compiling. **Added** - `FlushAll(ctx, flushers, evts)`, the concurrent fan-out `Producer` and `Server` deliver through, for programs that run their own server. Every flusher runs to completion and the errors are joined with the flusher names. - `DecodeEvents(w, r, maxBytes)`, `WriteServerError(w, status, code, message)`, `MaxRequestBody` and the `ErrorCode*` constants (`METHOD_NOT_ALLOWED`, `UNAUTHENTICATED`, `INVALID_ARGUMENT`, `PAYLOAD_TOO_LARGE`, `RESOURCE_EXHAUSTED`, `UNAVAILABLE`), so a server that speaks the `Server` protocol writes the same bytes and a `Client` decodes them as `ServerError`. - `ClientOptions.Token` (`token` in JSON, `Token` in YAML and TOML) sends one bearer token instead of a username and password; `ServerOptions.Tokens` (`tokens` / `Tokens`) lists the accepted ones, compared in constant time, alongside `BasicAuth`. Either credential unlocks a request when both are set. **Fixed** - A batch above 32 MiB is refused with 413 and `PAYLOAD_TOO_LARGE` instead of 400 and `INVALID_ARGUMENT`, so a sender can tell "shrink the batch" from "bad JSON". ### v0.0.6 Go 1.27 refactor. The public API changed in several places; every change is listed here with what a caller must do. **Breaking changes** - `NewInbox`, `NewSlack` and `NewTwilio` now return `(T, error)` and validate their required fields, like `NewDiscord` and `NewSQL` already did. Callers add an error check. - `Producer.WithErrorLogger` is gone. Set `ProducerOptions.OnError` instead. The default logs through `log/slog`. - `Server.WithErrorListener` and `Server.WithBasicAuth` are gone. Set `ServerOptions.OnError` and `ServerOptions.BasicAuth` (a username to password map) instead. - `Server.Handler` and `Server.HandlerFunc` are gone. `Server` is a plain `http.Handler`. When `BasicAuth` is set, GET requests are authenticated too, so `Client.Test` now verifies the credentials. - `EventConsumer` and `EventConsumerFunc` are gone. `NewServer` takes `EventFlusher`s and `EventFlusherFunc` adapts a function. Every integration lost its `ListenEvents` method. - `EventFlusher` is now the single method `FlushEvents`. `Test` moved to the optional `Tester` interface and `String` is optional. Custom flushers keep working; they just have fewer methods to implement. - `Zone` and `ZoneOffset` (`LocationUTCOffset` in YAML) are replaced by `TimeZone`, an IANA name such as `Europe/Athens`, on `InboxOptions`, `DiscordOptions` and `TwilioOptions`. `SlackOptions` never used its zone and no longer has one. - `NewSQL` and `NewSQLWithDatabase` take a `context.Context` first. - Mentions are a typed `Event.Mentions []string` (JSON `mentions`) instead of a `Mentions` field inside `Fields`. `WithMentions` keeps its name. - Removed: `Chunk` (use `slices.Chunk`), `Now`, `EventGroup` and `RemoveEventByFunc`, `DefaultErrorLogger`, `CollapseText`, the never-populated `Frame` fields (`Symbol`, `Colno`, `PreContext`, `ContextLine`, `PostContext`, `Vars`), `Stacktrace.FramesOmitted`, and the never-set `ServerError` fields (`Details`, `Validation`, `Data`). - `Frame.AbsPath` is now the plain file path; the line number lives only in `Lineno`. - `EmailTemplate` is an `html/template` and escapes its data. Template data changed: each event's `Stacktrace` is a list of frames with `Function`, `Module`, `Location` (`path:line`) and `Link` (`vscode://file/path:line`). - `BufferInterval` values below one second are honoured instead of being replaced by 5s. Zero or negative still means 5s. - `SendEvent` keeps an event's own `ProjectName` and only fills in the producer's when empty. - Debug events are dropped at `SendEvent` time while debug mode is off, instead of at flush time. `Debugging` sessions therefore deliver their event reliably. **Fixed** - `WithMentions` followed by `SendEvent` panicked (`hash of unhashable type`), and any slice or map field value did the same. Field de-duplication no longer hashes values. - Slack mentions were never rendered (case mismatch on the field name). - Events received by `Server` were delivered on the already-cancelled request context. - One failing flusher cancelled the others mid-flight. Flushers now run independently and `OnError` receives one joined error naming each failed flusher. - Concurrent `SendEvent` and `Close` could panic with "send on closed channel"; `Debugging` raced with `Close` on a `WaitGroup`. - Every flusher sorted the shared batch in place, concurrently. Batches are now sorted once and are read-only for flushers. - `Inbox.Test` failed against real servers because it authenticated without STARTTLS. - `WithFieldsPrepended` and `WithFields` dropped every pair when given an odd number of arguments and mishandled `Fields` values in the list. - `Close(ctx)` ignored its context. It now cancels the in-flight flush and returns `ctx.Err()` when the deadline passes; the shutdown completes in the background. - Emails carry `From`, `To`, `Date` and an encoded `Subject`; group, file and mention order is deterministic; the 405 response sets `Allow` correctly; Discord calls honour the context. **Added** - `Producer.Test(ctx)` runs the `Test` of every flusher that implements `Tester`. - `ErrClosed`, returned by `SendEvent` and `Debugging` after `Close`. - `NewSlack(opts, ...slack.Option)` and `NewClient(opts, ...httpclient.Option)` accept client options, for proxies and tests. - Event IDs are UUID v7 from the standard library `uuid` package. **Dependencies** - Removed `github.com/google/uuid` and `golang.org/x/sync`. The `go/build` import is gone too, so binaries no longer link the Go parser. - Requires Go 1.27. - Requires `github.com/kataras/basicauth` v0.0.8. The `BasicAuth` map of `ServerOptions` is checked through `basicauth.AllowUsersMap`, which now compares passwords in constant time and compares an unknown username against a decoy, so the response time does not reveal which usernames exist. **Migration for the known consumers** - Every wrapper that does `patrol.NewProducer(opts, flushers...).WithErrorLogger(fn)` becomes `patrol.NewProducer(patrol.ProducerOptions{..., OnError: fn}, flushers...)`, and every `patrol.NewInbox(o)`, `patrol.NewSlack(o)`, `patrol.NewTwilio(o)` call gains an error check. `SlackOptions.AcceptChannelEvent`, `Event.Error`, `WithStacktrace(nil)`, `WithFieldsPrepended`, `WithStringField`, `WithField`, `WithError`, `WithMessage`, `NewEvent`, `NewException`, `NewStacktrace`, `AddSkipModuleFrames`, `SendEvent`, `Close` and `Debugging` are unchanged. ## Frequently asked questions **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. ## Billing questions **What counts as an event?** One element of the JSON array your producer posts. A batch of 40 events is 40, whatever their type. Debug events count only when the project keeps them, which is a per-project switch. Events refused because the batch was malformed, too large or over quota are not stored and are not billed, though they are counted as rejected on the usage page so a misconfigured sender is visible. **What happens when I pass the monthly limit?** Nothing is dropped on the spot. Patrol Cloud keeps accepting and storing events for 24 hours past the limit and marks them as over quota, so an incident in the last week of the month does not go dark. After that window the ingest endpoint answers 429 with RESOURCE_EXHAUSTED, which a Client surfaces as a ServerError. Your own destinations, configured in the library, keep working the whole time. **Why do Pro and Business have no price yet?** Because they are not priced yet. The limits are settled and the plans are built; the amount is not, and a number on this page would be a guess. Start on the free plan, and the console will show the price and the upgrade button the moment there is one to show. **Do I need a plan to use the library?** No. The library is BSD-3-Clause and complete on its own: Slack, Discord, Twilio, email, SQL and your own patrol server all work with no account here. A plan buys the hosted side, which is storage, the console, per-project ingest keys and the fan-out running somewhere other than your process. ## Contact - Email: contact@hellenic.dev - GitHub issues: https://github.com/kataras/patrol/issues - Company: Hellenic Development, https://hellenic.dev