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
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:
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)
}NewSlogHandlerpasses every record to the handler you give it, unchanged, and sends a copy to dlogify. Passnilto send only to dlogify.- Give it a handler you create, such as
slog.NewTextHandler(os.Stderr, nil). Do not wrapslog.Default().Handler(): Go's default handler writes through thelogpackage, whichslog.SetDefaultpoints back at the new handler, and the nextlog.Printhangs (a Go limitation, golang/go#61892). - Attributes become record attributes; groups become dotted keys such as
req.id. - An attribute whose value is an
errorbecomes the record's exception: its type (*fs.PathError), its message, the stack of the logging call and oneCaused byline per wrapped error. - Go has no exit hook. Without
defer logify.Shutdown(…)inmain, records still queued whenmainreturns are lost.os.Exitskips deferred calls, so calllogify.Flush(2 * time.Second)before it. Initinstalls nothing by itself: you choose what to capture.
You can also log directly:
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:
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 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:
go install dlogify.com/sdk/go/cmd/logify@latest
LOGIFY_API_KEY=lgf_live_… logify run -- ./server --port 8080The 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:
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: 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:
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.DefaultTransportdoes not see its requests. - The client's own diagnostics go to stderr, never through
logorslog.
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.