Help us shape the future of Go! Take the 2026 Go Developer Survey

zharp

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 18 Imported by: 0

README

Zharp Logs for Go

Send your Go app's logs and panics to Zharp. No dependencies — only the standard library — and Go 1.21 and up. Works on its own or as a log/slog handler.

Full documentation: zharp.io/docs/sdks/go


1. Install

go get github.com/zharp-io/zharp-go-lib
import zharp "github.com/zharp-io/zharp-go-lib"

2. Get an API key

  1. In Zharp, open Integrations & API and choose Main API keys.
  2. Click Create API key, give it a name, and tick Ingest only. That key can send logs but can't read anything, so it's safe in a server's environment.
  3. Click Create key and copy the key — it's shown only once.

Create API key, with Ingest only ticked The new key, shown once with a Copy button

3. Start it

Call Init first thing in main, and defer Close — Go runs nothing at exit by itself, so without it the last logs are lost:

func main() {
	zharp.Init(zharp.Config{
		APIKey:      "your-ingest-only-key",
		Service:     "checkout-api",
		Environment: "production",
	})
	defer zharp.Close()
	defer zharp.Recover() // send a panic before the program dies of it

	// …
}

Or leave the config empty and set the environment instead:

ZHARP_API_KEY=your-ingest-only-key
ZHARP_SERVICE=checkout-api
ZHARP_ENV=production
zharp.Init(zharp.Config{})

You can mix them: what's set in Config is used, and anything left empty is read from the environment. Without a service name, the last part of your module path is used (github.com/acme/checkout-api → checkout-api).

When it starts, it prints one line to stderr so you know it's working:

[Zharp] Logging active — service "checkout-api", environment "production"

With no API key anywhere it prints how to give it one and sends nothing. It never returns an error or panics.

4. Log

zharp.Info("User signed in", zharp.Fields{"user_id": 42})
zharp.Warn("Slow network detected", zharp.Fields{"latency_ms": 3200})

if err := charge(order); err != nil {
	zharp.Error("Payment failed", zharp.Fields{"order_id": order.ID, "err": err})
	// or the error alone:
	zharp.CaptureError(err)
}

Debug and Fatal are there too — Fatal logs at fatal but, unlike log.Fatal, doesn't exit. Fields are optional and are kept with the log. An error — as the message or among the fields — brings its type and a stack: Go errors don't carry one, so it's where the error was logged; an error that formats its own with %+v (as github.com/pkg/errors does) keeps that one.

Logs are sent from a background goroutine in batches every 5 seconds; Error and Fatal go at once. Logging never blocks on the network and never panics.

5. See your logs

Sign in at zharp.io, open Logs, and pick your service in the service dropdown.

The Logs page on zharp.io


log/slog

Send your slog logs to Zharp:

slog.SetDefault(slog.New(zharp.NewSlogHandler(nil)))

slog.Warn("Stock low", "sku", "SKU-1")
slog.Error("Pricing failed", "err", err)

Attributes become the log's fields (groups nested), an error attribute brings its stack, and levels map as you'd expect. NewSlogHandler(&zharp.SlogOptions{Level: slog.LevelWarn}) sends only WARN and up; the default is INFO.

To keep printing to the console as well, pass each record to both handlers — a few lines:

type both []slog.Handler

func (b both) Enabled(ctx context.Context, l slog.Level) bool {
	for _, h := range b {
		if h.Enabled(ctx, l) {
			return true
		}
	}
	return false
}
func (b both) Handle(ctx context.Context, r slog.Record) error {
	for _, h := range b {
		if h.Enabled(ctx, r.Level) {
			_ = h.Handle(ctx, r.Clone())
		}
	}
	return nil
}
func (b both) WithAttrs(a []slog.Attr) slog.Handler {
	out := make(both, len(b))
	for i, h := range b {
		out[i] = h.WithAttrs(a)
	}
	return out
}
func (b both) WithGroup(g string) slog.Handler {
	out := make(both, len(b))
	for i, h := range b {
		out[i] = h.WithGroup(g)
	}
	return out
}

slog.SetDefault(slog.New(both{slog.NewTextHandler(os.Stderr, nil), zharp.NewSlogHandler(nil)}))

Where the Zharp agent already collects your app's output (Docker, journald, Kubernetes), send logs through one of them, not both, or every line arrives twice.

Panics

Go has no global hook for panics, so capturing them takes one line where they can happen:

  • defer zharp.Recover() in main, and at the top of any goroutine you start. A panic is sent as a fatal log with its stack, then it carries on — Go prints it and the program ends as it would have.
  • zharp.Middleware(handler) around an HTTP handler. A panic in a request is sent as an error log with the stack and the request's method and path, then net/http handles it as usual — it ends that request and keeps serving.
http.ListenAndServe(":8080", zharp.Middleware(mux))

Options

Config field Environment variable Default
APIKey ZHARP_API_KEY —
Service ZHARP_SERVICE, then OTEL_SERVICE_NAME the last part of your module path, else unknown-service
Environment ZHARP_ENV none
AppVersion ZHARP_VERSION none
Source ZHARP_SOURCE none
Endpoint ZHARP_ENDPOINT https://api.zharp.io/api/v1
FlushInterval — 5 seconds
MaxBatchSize — 50
HTTPClient — a client with a 15-second timeout

A field set in Config wins. ZHARP_DISABLED=true turns the SDK off.

What's sent automatically

Every log carries a session ID (the same for one run of the program) and where it ran: hostname, process ID, Go version, OS and architecture. Add your own fields to every log with zharp.SetContext(zharp.Fields{"region": "eu-west"}); start a new session ID with zharp.ResetSessionID().

zharp.Flush(5 * time.Second) sends what's queued and waits for it — call it before a serverless function returns.

Releasing (maintainers)

Go modules are published by tag — there's no registry upload. To release:

  1. Update Version in zharp.go and CHANGELOG.md, run go vet ./... && go test ./..., commit and push.
  2. Tag and push the tag: git tag v0.1.1 && git push origin v0.1.1.
  3. Optionally ask the module proxy to pick it up now, so it shows on pkg.go.dev straight away: GOPROXY=https://proxy.golang.org go list -m github.com/zharp-io/zharp-go-lib@v0.1.1.

A version is permanent once fetched — the proxy caches it forever — so never move or reuse a tag; release a new one instead.

License

MIT © Zharp

Documentation

Overview

Package zharp sends your Go app's logs and panics to Zharp.

Start it once in main, and close it on the way out so the last logs are sent:

func main() {
	zharp.Init(zharp.Config{APIKey: "your-ingest-only-key", Service: "checkout-api"})
	defer zharp.Close()

	zharp.Info("User signed in", zharp.Fields{"user_id": 42})
}

Logs are queued and sent in batches from a background goroutine; Error and Fatal go at once. Logging never blocks on the network and never panics. To send log/slog records, use NewSlogHandler; to capture panics, Recover and Middleware.

Index

Constants

View Source
const Version = "0.1.1"

Version is this package's version.

Variables

This section is empty.

Functions

func CaptureError

func CaptureError(err error, fields ...Fields)

CaptureError logs err at error with its type and stack. A nil err does nothing.

if err := charge(order); err != nil {
	zharp.CaptureError(err, zharp.Fields{"order_id": order.ID})
}

func Close

func Close()

Close sends everything queued (waiting at most 5 seconds) and stops. Defer it in main: Go runs nothing at exit by itself, so logs still queued when main returns would be lost.

func Debug

func Debug(message any, fields ...Fields)

Debug logs at debug. fields is optional.

func Error

func Error(message any, fields ...Fields)

Error logs at error and sends at once. An error as the message, or among the fields, brings its stack.

func Fatal

func Fatal(message any, fields ...Fields)

Fatal logs at fatal and sends at once. Unlike log.Fatal it doesn't end the program.

func Flush

func Flush(timeout time.Duration) bool

Flush sends everything queued and waits up to timeout for it. It reports whether all of it went. Normally not needed: logs go out every few seconds and errors at once — but call it before a serverless function returns.

func Info

func Info(message any, fields ...Fields)

Info logs at info. fields is optional.

func Init

func Init(cfg Config)

Init starts sending logs. Call it once, as early as possible in main, and defer Close.

It prints one line to stderr:

[Zharp] Logging active — service "checkout-api", environment "production"

With no API key anywhere it prints how to give it one and starts nothing; ZHARP_DISABLED=true turns it off whatever the code says. It never returns an error or panics. Calling it again replaces the running setup; what the old one had queued is still sent.

func IsActive

func IsActive() bool

IsActive reports whether Init succeeded and logs are being sent.

func Log

func Log(level Level, message any, fields Fields)

Log logs at any level.

func LogService

func LogService(service string, level Level, message any, fields Fields)

LogService logs at any level under another service than the one set in Init.

func Middleware

func Middleware(next http.Handler) http.Handler

Middleware sends a panic in an HTTP handler as an error log with its stack and the request's method and path, then panics again so net/http handles it as it would have (it ends that request and keeps serving).

http.ListenAndServe(":8080", zharp.Middleware(mux))

func NewSlogHandler

func NewSlogHandler(opts *SlogOptions) slog.Handler

NewSlogHandler returns a log/slog handler that sends records to Zharp, at the matching level (slog.LevelWarn → warn; above slog.LevelError → fatal). Attributes become the log's fields, groups nested; an error attribute brings its stack. Start Zharp with Init first.

slog.SetDefault(slog.New(zharp.NewSlogHandler(nil)))
slog.Warn("Stock low", "sku", "SKU-1")

To keep printing to the console too, send records to both handlers — see the README.

func Recover

func Recover()

Recover sends a panic as a fatal log with its stack, waits briefly for it to go out, then panics again — so the program ends as it would have. Defer it first thing in main, and in any goroutine whose panics you want to see:

func main() {
	zharp.Init(zharp.Config{})
	defer zharp.Close()
	defer zharp.Recover()
	…
}

Go has no global hook for panics, so a goroutine without it ends the program unseen.

func ResetSessionID

func ResetSessionID()

ResetSessionID starts a new session ID, e.g. per job in a long-running worker.

func SessionID

func SessionID() string

SessionID is the ID every log of this run carries, so one run's logs can be found together.

func SetContext

func SetContext(fields Fields)

SetContext sets extra fields added to every log's context, e.g. a deployment's region. Replaces what was set before; pass nil to clear it.

func Warn

func Warn(message any, fields ...Fields)

Warn logs at warn. fields is optional.

Types

type Config

type Config struct {
	// APIKey is an Ingest only key from Integrations & API → Main API keys. Without one, nothing
	// starts.
	APIKey string
	// Service is the name logs are filed under. Defaults to the last part of the main module's
	// path (github.com/acme/checkout-api → checkout-api), else "unknown-service".
	Service string
	// Environment tags every log, e.g. "production". Not guessed when unset.
	Environment string
	// AppVersion tags every log, so you can filter to one release.
	AppVersion string
	// Source tags every log with what wrote it.
	Source string
	// Endpoint is where logs are sent. Defaults to https://api.zharp.io/api/v1.
	Endpoint string
	// FlushInterval is how long logs wait to be sent together. 5 seconds when zero.
	FlushInterval time.Duration
	// MaxBatchSize is the most logs sent in one request. 50 when zero.
	MaxBatchSize int
	// HTTPClient sends the requests. One with a 15-second timeout when nil.
	HTTPClient *http.Client
}

Config holds the settings for Init. Every field is optional: anything left empty is read from the environment (ZHARP_API_KEY, ZHARP_SERVICE then OTEL_SERVICE_NAME, ZHARP_ENV, ZHARP_VERSION, ZHARP_SOURCE, ZHARP_ENDPOINT), then defaulted.

type Fields

type Fields map[string]any

Fields are extra data kept with a log. Values are copied when the log is made, so changing the map afterwards doesn't change a log waiting to be sent. An error among them brings its stack.

type Level

type Level string

Level is how bad a log is.

const (
	LevelDebug Level = "debug"
	LevelInfo  Level = "info"
	LevelWarn  Level = "warn"
	LevelError Level = "error"
	LevelFatal Level = "fatal"
)

type SlogOptions

type SlogOptions struct {
	// Level is the lowest level sent. slog.LevelInfo when nil.
	Level slog.Leveler
}

SlogOptions configures NewSlogHandler.

Directories

Path Synopsis
examples
basic command
Every kind of log, then a panic.
Every kind of log, then a panic.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL