Skip to content

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.

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

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.

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.

Four things, in this order, before the event reaches the buffer:

  1. Stamps CapturedAt with the current Unix second. Anything you set yourself is overwritten.
  2. Fills in ProjectName from the producer when the event has none. An event with its own name keeps it.
  3. Removes duplicate fields: same name and equal value, first occurrence kept. Comparison is == for comparable values and reflect.DeepEqual for 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.
  4. Drops the event when it is Debug and debug mode is off, returning nil.

At flush time the producer sorts the batch by CapturedAt and stamps SentAt. A Server that receives the batch stamps ReceivedAt.

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.

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