---
url: https://simpledatalogs.com/docs/sdks/go.md
description: >-
  The simpledatalogs Go module for services, workers and CLIs; install, track
  events, a slog handler, Flush and Close, delivery and caps.
---

# Go

The `github.com/XKD-Labs/simpledatalogs-go` module sends events and log lines from Go services, workers and CLIs. It uses only the standard library and never panics into your app.

* Go 1.22+.
* A `slog.Handler` for your logs.
* A nil `*Client` is a no-op, so a missing key never crashes the app.

## Install

```sh
go get github.com/XKD-Labs/simpledatalogs-go
```

::: info Early access
The module has no tagged release yet. Until it does, fetch the latest commit:

```sh
go get github.com/XKD-Labs/simpledatalogs-go@main
```

:::

## Quick start

```go
import simpledatalogs "github.com/XKD-Labs/simpledatalogs-go"

sdl, err := simpledatalogs.New(simpledatalogs.Config{}) // reads SIMPLEDATALOGS_KEY
if err != nil {
	log.Printf("simpledatalogs is off: %v", err)
}
defer sdl.Close(context.Background())

sdl.Track("signup", simpledatalogs.Props{"plan": "pro"}, simpledatalogs.WithUserID("42"))
sdl.Log(simpledatalogs.LevelError, "Payment failed", simpledatalogs.Props{"order_id": 7})
```

A background goroutine sends every 5 seconds. `Close` sends what is left; call it before `main` returns. Use a **secret key** (`sdl_sec_…`) on servers.

`New` returns an error only for a missing key or a malformed endpoint, and then a nil `*Client` whose methods do nothing.

## Events

```go
sdl.Track("checkout", simpledatalogs.Props{"plan": "pro", "seats": 3, "trial": false},
	simpledatalogs.WithUserID(strconv.Itoa(user.ID)),
	simpledatalogs.WithTime(finishedAt),
)
```

* Every line gets a UUID `event_id` and a `ts` when you call `Track` or `Log`, so a retry never counts twice.
* Props are strings, numbers, booleans and named types over them; `fmt.Stringer`s send their text. Anything else is skipped with a warning. At most 100 props.

## Logs (slog)

```go
logger := slog.New(simpledatalogs.NewHandler(sdl, &simpledatalogs.HandlerOptions{Level: slog.LevelWarn}))

logger.Error("Payment failed", "order_id", 7, "user_id", user.ID, "err", err)
```

* Each record becomes one log line with its level, time and message.
* Attributes become props; groups become dotted names (`http.status`). Errors and `Stringer`s send their text.
* Levels map to `debug`, `info`, `warning`, `error`, and `critical` from `slog.LevelError+4`.
* To keep your current output too, fan out to both handlers.

`sdl.Log` takes `LevelDebug`, `LevelInfo`, `LevelNotice`, `LevelWarning`, `LevelError` or `LevelCritical`.

## Users

Identity is per call: `WithUserID`, `WithDeviceID`, `WithSessionID` and `WithAnonymousID`. In slog records, top-level `user_id`, `device_id`, `session_id` and `anonymous_id` attributes fill the same fields.

## The opt-out

```go
sdl.SetEnabled(false) // deletes every unsent line and ignores new ones
sdl.SetEnabled(true)  // collects again
```

To keep a client silent in tests or local runs, set `Disabled: true` in `Config`.

## Flush and Close

```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := sdl.Close(ctx); err != nil {
	log.Printf("simpledatalogs: %v", err) // errors.Is(err, simpledatalogs.ErrUndelivered)
}
```

* With a deadline, `Flush` and `Close` retry until the queue is empty or the deadline passes.
* Without one, they try once and stop at the first failure.
* In an HTTP server, call `Close` after `server.Shutdown`, so the last requests' events go too.
* `sdl.Pending()` counts lines not yet confirmed.

## Configuration

| `Config` field | Default | What it does |
|---|---|---|
| `Key` | `SIMPLEDATALOGS_KEY` | The ingest key |
| `Endpoint` | `SIMPLEDATALOGS_ENDPOINT`, else `https://edge.simpledatalogs.com` | The ingest host; `/v1/ingest` is added |
| `FlushInterval` | `5s` | How often the background goroutine sends |
| `MaxBatchEvents` | `50` | Lines per request |
| `MaxRequestBytes` | `256000` | Bytes per request before gzip |
| `MaxQueueEvents` / `MaxQueueBytes` | `10000` / `10000000` | The queue's caps |
| `RequestTimeout` | `15s` | Per request |
| `DisableGzip` | `false` | Bodies of 1 KB and more are gzipped unless set |
| `DefaultProps` | none | Props added to every line, under the call's own |
| `HTTPClient` | its own | Copied; redirects are never followed |
| `Logger` | text on stderr | Where the SDK's own warnings go, once per kind |
| `Disabled` | `false` | Keeps the client silent |

## Privacy

* The SDK sends only what you pass: no IP addresses, hostnames, environment variables or request data.
* slog attributes reach ingest as you logged them, so leave personal data out of them.
* See [Privacy for senders](/reference/privacy) for what belongs in `user_id`.

## How it behaves

### Batching

Every 5 seconds, or at once when 50 lines wait. Each request stays under 256 KB before gzip. No call waits on the network.

### Memory only

The queue lives in memory, so lines still queued when the process dies are lost. For log files that must survive crashes, ship the files with [Vector or Fluent Bit](/integrations/log-shippers).

### Errors and offline

| Answer | What the SDK does |
|---|---|
| `202` | Removes the lines |
| `401` | Keeps every line and re-checks the key after 1 minute, then every 10 minutes |
| `413` | Halves the batch; a single line still too large is dropped |
| `415` | Drops the batch, with one warning |
| Anything else, a timeout, offline | Backs off with full jitter from 1 second to 5 minutes, never sooner than `Retry-After` |

### Caps

The queue keeps at most **10,000 lines or 10 MB**. Past that, the oldest unsent lines are dropped, with a warning.

## Troubleshooting

The SDK's warnings go to stderr, once per kind; set `Config.Logger` to route them elsewhere. For local development, set `SIMPLEDATALOGS_ENDPOINT=http://localhost:4001`.

Package docs are on [pkg.go.dev](https://pkg.go.dev/github.com/XKD-Labs/simpledatalogs-go).

## Next steps

* [Go quickstart](/quickstarts/go): the shortest path.
* [Text log lines](/api/log-lines): how levels and messages are stored.
* [Privacy for senders](/reference/privacy): ids, not emails.
