dlogify docs

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:

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).
  • 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:

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 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:

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.

VariableOptionDefault
LOGIFY_API_KEYAPIKeyRequired; without it the client is disabled.
LOGIFY_ENDPOINTEndpointhttps://api.dlogify.com
LOGIFY_SERVICEServiceOTEL_SERVICE_NAME, then the last element of your main module's path, then the executable name
LOGIFY_ENVIRONMENTEnvironmentdefault
LOGIFY_RELEASEReleaseempty
LOGIFY_MIN_LEVELMinLevelinfo (trace, debug, info, warn, error, fatal)
LOGIFY_DISABLEDDisabledoff (1 or true turns the client into a no-op)
LOGIFY_DEBUGDebugoff (1 or true prints the client's diagnostics to stderr)
LOGIFY_REDACT_DEFAULTSRedactDefaultsoff
LOGIFY_REDACT_KEYSRedactKeysempty

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

On this page