Skip to content

HTTP client

patrol.Client is a flusher that posts the batch to a patrol Server. POST / with the events as a JSON array; GET / for the connectivity check, which answers the literal body OK.

It is how a service keeps its credentials to itself. The Slack token, the SMTP password and the Twilio account live on the machine running the Server; every service that reports events holds one URL and one credential.

patrol.NewClient(opts patrol.ClientOptions, clientOpts ...httpclient.Option) *patrol.Client

No error. NewClient has no required field and validates nothing, so an empty BaseURL is not caught here: it fails at the first request. The variadic httpclient.Option values are applied after the defaults, which is where a timeout, a retry policy or a custom transport goes.

The returned *patrol.Client embeds *httpclient.Client, so its methods are available if you need to call the server yourself.

Field Type JSON YAML and TOML Required
BaseURL string base_url BaseURL in practice, yes
BasicAuth.Username string basic_auth.username BasicAuth.Username no
BasicAuth.Password string basic_auth.password BasicAuth.Password no
Token string token Token no

Token is sent as Authorization: Bearer <token> on every request and the server accepts it when it is listed in its own Tokens. BasicAuth sends a username and password instead. When both are set, Token wins and the username and password are never sent. Set one.

A server with neither BasicAuth nor Tokens configured accepts anything, so a client with no credential is a valid setup on a private network.

client.yml
BaseURL: http://patrol.internal:8080
Token: pk_live_2f8c...

From the library’s README, with a username and password:

// source: README.md#L131-L135
client := patrol.NewClient(patrol.ClientOptions{
BaseURL: "http://patrol.internal:8080",
BasicAuth: patrol.BasicAuthOptions{Username: "user", Password: "pass"},
})
producer := patrol.NewProducer(patrol.ProducerOptions{ProjectName: "api"}, client)

A failed request comes back as a patrol.ServerError, which carries ErrorCode and Message. The codes are exported constants, so a sender can switch on them instead of on strings:

package main
import (
"context"
"errors"
"log"
"os"
"time"
"github.com/kataras/patrol"
)
func main() {
ctx := context.Background()
client := patrol.NewClient(patrol.ClientOptions{
BaseURL: "https://api.patrol.hellenic.dev",
Token: os.Getenv("PATROL_KEY"),
})
producer := patrol.NewProducer(patrol.ProducerOptions{
ProjectName: "checkout-api",
BufferInterval: 5 * time.Second,
OnError: onError,
}, client)
defer producer.Close(ctx)
if err := producer.Test(ctx); err != nil {
log.Println("patrol:", err)
}
}
func onError(err error) {
serverErr, ok := errors.AsType[patrol.ServerError](err)
if !ok {
log.Println("patrol:", err)
return
}
switch serverErr.ErrorCode {
case patrol.ErrorCodeUnauthenticated:
log.Println("patrol: the credential was refused")
case patrol.ErrorCodePayloadTooLarge:
log.Println("patrol: the batch is too large, lower MaxBatchSize")
case patrol.ErrorCodeResourceExhausted:
log.Println("patrol: over quota")
default:
log.Println("patrol:", serverErr.ErrorCode, serverErr.Message)
}
}

errors.AsType reaches through both the flusher-name prefix the producer adds and the joined error it builds when several flushers fail at once.

producer.Test(ctx) sends GET / and expects OK. When the server has credentials configured, that GET is authenticated too, so this check covers the credential as well as the route.

An empty batch sends nothing. FlushEvents returns early on a zero-length batch, so an idle producer makes no requests at all.

A 200 is not a delivery. The server answers as soon as it has decoded the batch and delivers in the background. What you learn from a successful POST is that the server accepted the events, not that Slack got them. Errors on that side reach the server’s own OnError.

32 MiB is the ceiling. patrol.MaxRequestBody is 32 MiB and a larger body is refused with 413 and PAYLOAD_TOO_LARGE, before any decoding. Events carrying files are what gets a batch there; MaxBatchSize is the knob.

Retries are yours. Nothing is retried and nothing is buffered to disk. A flush that fails is reported to OnError and its events are gone. A sender that cannot lose events wraps the client in its own flusher with a queue.