Events
An event is a struct you build with chained methods and hand to SendEvent. Every builder returns the event, so they chain in any order, and every one of them mutates the receiver rather than copying it.
The four types
Section titled “The four types”// source: event_type.go#L3-L16// EventType represents the type of an event.type EventType uint8
const ( // None indicates an undefined event type. None EventType = iota // Info represents informational events. Info // Error represents error events. Error // Debug represents debugging events. They are only delivered while the // Producer's debug mode is on. Debug)None is zero, which is what patrol.NewEvent(patrol.None) and an event decoded from a JSON body with no type both get. It is a real type, not a rejection: it is delivered like any other, and it renders as none.
Error is what NewException and WithError set. Debug is the only type with special handling: it is dropped at SendEvent while debug mode is off. See Debug events.
Starting an event
Section titled “Starting an event”| Call | What you get |
|---|---|
patrol.NewEvent(patrol.Info) |
A fresh event of that type with a UUID v7 id |
patrol.NewException(err) |
Type Error, the error kept, a stacktrace captured, the message set to err.Error() |
NewException(nil) is the one call that surprises people. WithError ignores a nil error, so you get an event of type Error with an empty message and no stacktrace. Check the error before you wrap it.
The builders
Section titled “The builders”| Method | Effect |
|---|---|
WithMessage(string) |
Sets the message. The one line every destination shows first |
WithField(name, value any) |
Appends one field. value is any and is rendered with %v |
WithFields(keyValues ...any) |
Appends alternating name and value pairs. A Field or a Fields anywhere in the list is appended as is |
WithFieldsPrepended(keyValues ...any) |
The same, in front of the existing fields |
WithStringField(key, value string) |
Appends only when value is not empty. For optional context that is usually blank |
WithUserID(string) |
WithField("UserID", ...) |
WithUserEmail(string) |
WithField("UserEmail", ...) |
WithError(err) |
Sets type Error, keeps the error, captures a stacktrace, sets the message when there is none yet |
WithDebug(message) |
Sets type Debug, sets the message, captures a stacktrace |
WithStacktrace(*Stacktrace) |
Attaches one, or removes it when passed nil |
WithFile(name, content string) |
Attaches a file by name. Slack uploads it; the others ignore it |
WithMentions(ids ...string) |
Slack user ids to ping. Trimmed, de-duplicated, empty values ignored |
WithProjectName(string) |
Overrides the producer’s project name for this event |
WithFields follows the log/slog rules for a malformed list: a name with no value, or a name that is not a string, becomes a field called !BADKEY carrying that value. Nothing panics and nothing is silently dropped, so a bad call shows up in the message rather than in a stack trace.
package main
import ( "context" "errors" "log"
"github.com/kataras/patrol")
func main() { ctx := context.Background() producer := patrol.NewProducer(patrol.ProducerOptions{ProjectName: "checkout-api"}) defer producer.Close(ctx)
e := patrol.NewException(errors.New("charge declined")). WithUserEmail("user@example.com"). WithFields("OrderID", 84213, "Attempt", 2, "Gateway", "stripe"). WithStringField("PromoCode", ""). WithFile("request.json", `{"amount":1299,"currency":"eur"}`). WithMentions("U0123456789")
if err := producer.SendEvent(ctx, e); err != nil { log.Fatal(err) }}PromoCode is empty, so WithStringField skips it and the message carries three fields plus UserEmail.
What SendEvent does to it
Section titled “What SendEvent does to it”Four things, in this order, before the event reaches the buffer:
- Stamps
CapturedAtwith the current Unix second. Anything you set yourself is overwritten. - Fills in
ProjectNamefrom the producer when the event has none. An event with its own name keeps it. - Removes duplicate fields: same name and equal value, first occurrence kept. Comparison is
==for comparable values andreflect.DeepEqualfor the rest, so a slice or a map as a field value is safe. An earlier version hashed values here and panicked on exactly that. - Drops the event when it is
Debugand debug mode is off, returningnil.
At flush time the producer sorts the batch by CapturedAt and stamps SentAt. A Server that receives the batch stamps ReceivedAt.
On the wire
Section titled “On the wire”This is the JSON a Client posts and a Server decodes. Error is not in it: the Go error is kept in memory for the flushers and is tagged json:"-", because an arbitrary error value does not round trip.
{ "id": "0199a3c1-8f0e-7b3a-9c21-0b8f1e7d4a55", "type": 2, "project_name": "checkout-api", "captured_at": 1757980800, "sent_at": 1757980805, "received_at": 1757980805, "message": "charge declined", "fields": [{ "name": "UserEmail", "value": "user@example.com" }], "files": { "request.json": "{\"amount\":1299}" }, "mentions": ["U0123456789"], "stacktrace": { "frames": [] }}The three timestamps are Unix seconds, not milliseconds. type is the numeric constant: 0 none, 1 info, 2 error, 3 debug.
What each destination renders
Section titled “What each destination renders”Not every field reaches every destination. This is the table worth checking before you build an event around one of them.
| Message | Fields | Stacktrace | Files | Mentions | Capture time | |
|---|---|---|---|---|---|---|
| Slack | yes, collapsed over 500 bytes | yes | yes, with editor links | uploaded, linked | yes | no |
| Discord | yes | yes | yes | no | no | yes |
| Twilio | yes | yes | yes | no | no | yes |
| yes | yes | yes, with editor links | no | no | yes | |
| SQL | column | column, JSON | column, JSON | no | no | column |
| HTTP client | the whole event, as JSON |
Slack is the only destination that uploads files and the only one that renders mentions, because the ids in WithMentions are Slack user ids. Slack is also the only one that shows no capture time: the message carries Slack’s own posting time instead.
Next: debug events.