# Go

> Send logs and panics from Go with the dlogify module.

By the end of this page your Go service sends its logs and panics to dlogify. The module `dlogify.com/sdk/go` requires Go 1.22 or later and has no dependencies outside the standard library.

## Install

```bash
go get dlogify.com/sdk/go
export LOGIFY_API_KEY=lgf_live_…
```

The package name is `logify`.

## With the SDK

Call `logify.Init` once at the start of `main`, defer `Shutdown` so queued records are sent before the process ends, and defer `Recover` to send a panic:

```go title="main.go"
package main

import (
	"errors"
	"log/slog"
	"os"
	"time"

	logify "dlogify.com/sdk/go"
)

func main() {
	logify.Init(logify.Options{Service: "billing"})
	defer logify.Shutdown(2 * time.Second)
	defer logify.Recover()

	slog.SetDefault(slog.New(logify.NewSlogHandler(slog.NewTextHandler(os.Stderr, nil))))

	slog.Info("charging order", "order_id", "ord_1")
	if err := charge("ord_1"); err != nil {
		slog.Error("charge failed", "order_id", "ord_1", "err", err)
	}
}

func charge(orderID string) error {
	return errors.New("card declined for " + orderID)
}
```

- `NewSlogHandler` passes every record to the handler you give it, unchanged, and sends a copy to dlogify. Pass `nil` to send only to dlogify.
- Give it a handler you create, such as `slog.NewTextHandler(os.Stderr, nil)`. Do not wrap `slog.Default().Handler()`: Go's default handler writes through the `log` package, which `slog.SetDefault` points back at the new handler, and the next `log.Print` hangs (a Go limitation, [golang/go#61892](https://github.com/golang/go/issues/61892)).
- Attributes become record attributes; groups become dotted keys such as `req.id`.
- An attribute whose value is an `error` becomes the record's exception: its type (`*fs.PathError`), its message, the stack of the logging call and one `Caused by` line per wrapped error.
- Go has no exit hook. Without `defer logify.Shutdown(…)` in `main`, records still queued when `main` returns are lost. `os.Exit` skips deferred calls, so call `logify.Flush(2 * time.Second)` before it.
- `Init` installs nothing by itself: you choose what to capture.

You can also log directly:

```go
logify.Info("order placed", slog.String("order_id", "ord_1"))
logify.Error("charge failed", slog.Any("err", err))
logify.CaptureError(err) // level error, message "<type>: <message>"
logify.Log(logify.LevelWarn, "disk almost full")
```

`Fatal` records at level `fatal` but does not end the process. Calls before `Init` are dropped, and a second `Init` is ignored. `logify.New(opts)` creates an independent client with the same methods.

## The log package

`logify.CaptureStdLog()` sends what the standard `log` package writes, and keeps its output. A level word at the start of the line (`ERROR: …`) sets the level, `log.Fatal` and `log.Panic` are sent as `fatal`, everything else as `info`. If you already call `slog.SetDefault`, the `log` package goes through slog and the handler above sends it: do not call `CaptureStdLog` as well, or each `log` line is sent twice.

## Panics

A panic that nothing recovers ends the program. `defer logify.Recover()` sends it at level `fatal` with the stack that panicked, waits up to 2 seconds for the send, then panics again with the same value, so your program ends exactly as before. `recover` only works in the deferred function itself, so defer `Recover` directly.

Goroutines need their own: start them with `logify.Go(fn)`, which defers `Recover` for you.

For HTTP servers, wrap your handler:

```go
http.ListenAndServe(":8080", logify.Middleware(mux))
```

A panic in a handler is sent at level `error` with the method and path, and `net/http` handles it as usual. Frameworks with their own recovery middleware can call `logify.CaptureError` from it.

## zap, zerolog and logrus

Each adapter is a separate module, so your build only pulls the logging library you already use.

```go
// go get dlogify.com/sdk/go/zap
logger := zap.New(zapcore.NewTee(yourCore, logifyzap.NewCore()))

// go get dlogify.com/sdk/go/zerolog
logger := zerolog.New(zerolog.MultiLevelWriter(os.Stdout, logifyzerolog.Writer()))

// go get dlogify.com/sdk/go/logrus
logger.AddHook(logifylogrus.NewHook())
```

They send through the client created by `logify.Init`. Errors attached with `zap.Error` or `WithError` keep their type; zerolog writes errors as text, so they arrive without a type. Fatal and panic levels are sent before the library ends the process.

## Any process: logify run

`logify run` sends the output of any command, including an unmodified Go binary:

```bash
go install dlogify.com/sdk/go/cmd/logify@latest
LOGIFY_API_KEY=lgf_live_… logify run -- ./server --port 8080
```

The output still appears in your terminal exactly as before, and the exit code is the command's. Panics, stack traces and tracebacks stay together as single events, and JSON lines are read as structured records. A non-zero exit or a death by signal is reported as one more error, except a `SIGPIPE` after the reader of the output went away, as in `| head`. `logify run` is not available on Windows.

In a container, a panic in any goroutine ends the process before an SDK running inside it can do more than `Recover` allows. Run your binary under `logify run` to send every crash, including runtime errors such as concurrent map writes:

```dockerfile
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server . && CGO_ENABLED=0 GOBIN=/out go install dlogify.com/sdk/go/cmd/logify@latest

FROM gcr.io/distroless/static
COPY --from=build /out/server /out/logify /app/
ENTRYPOINT ["/app/logify", "run", "--", "/app/server"]
```

## Configuration

Options passed to `Init` win over environment variables, which win over the defaults. A boolean option set to `true` turns the setting on; `false` leaves the environment variable in charge.

| Variable | Option | Default |
|---|---|---|
| `LOGIFY_API_KEY` | `APIKey` | Required; without it the client is disabled. |
| `LOGIFY_ENDPOINT` | `Endpoint` | `https://api.dlogify.com` |
| `LOGIFY_SERVICE` | `Service` | `OTEL_SERVICE_NAME`, then the last element of your main module's path, then the executable name |
| `LOGIFY_ENVIRONMENT` | `Environment` | `default` |
| `LOGIFY_RELEASE` | `Release` | empty |
| `LOGIFY_MIN_LEVEL` | `MinLevel` | `info` (`trace`, `debug`, `info`, `warn`, `error`, `fatal`) |
| `LOGIFY_DISABLED` | `Disabled` | off (`1` or `true` turns the client into a no-op) |
| `LOGIFY_DEBUG` | `Debug` | off (`1` or `true` prints the client's diagnostics to stderr) |
| `LOGIFY_REDACT_DEFAULTS` | `RedactDefaults` | off |
| `LOGIFY_REDACT_KEYS` | `RedactKeys` | empty |

Also `Attributes` (added to every record) and `MaxQueueSize` (default 2000).

## Redaction

The rules are the same as in the [Node.js SDK](https://docs.dlogify.com/send-logs/redaction.md): `RedactKeys` (exact keys, ASCII case ignored) and `RedactKeyPatterns` (`*regexp.Regexp`) replace values with `<redacted>`, `RedactPatterns` replace matches in the message and in string attributes, `RedactDefaults` applies the same rules dlogify applies on arrival, and `BeforeSend` sees every record last:

```go
logify.Init(logify.Options{
	RedactKeys:     []string{"ssn"},
	RedactPatterns: []logify.RedactPattern{{Pattern: regexp.MustCompile(`MRN-\d{8}`), Replacement: "<mrn>"}},
	BeforeSend: func(e *logify.Event) *logify.Event {
		delete(e.Attributes, "customer_note")
		return e // return nil to drop the record
	},
})
```

`BeforeSend` runs on a goroutine of the client, one record at a time and in order, so it never blocks your logging calls and can safely log itself (those records are dropped). A `BeforeSend` that panics drops the record; the first failure is printed on stderr.

## Guarantees

- No function panics or returns an error, and logging never waits for the network: a background goroutine sends records in compressed batches, with retries for up to 5 minutes.
- If the key is rejected, the client stops for the rest of the process with one warning. Redirects are not followed: if the endpoint redirects, one warning names the URL to put in `LOGIFY_ENDPOINT`.
- The client uses its own HTTP transport, so instrumentation you add to `http.DefaultTransport` does not see its requests.
- The client's own diagnostics go to stderr, never through `log` or `slog`.

## What is sent

Only log records: message, level, time, the attributes you pass, the error's type, message and stack, and the service name, environment, release, host name and SDK version. The SDK makes no other network calls and opens no ports. See [What is sent](https://docs.dlogify.com/send-logs/what-is-sent.md).

Source: https://docs.dlogify.com/send-logs/go
