Note: This is a v0 release, API may change.
A Go client library for debuginfod servers.
Queries every configured server in parallel and returns the first authoritative response. An optional companion package provides on-disk caching.
See API documentation for details.
go get go.kacmar.sk/debuginfodRequires Go 1.25 or later.
import (
"go.kacmar.sk/debuginfod"
"go.kacmar.sk/debuginfod/key"
)
client, err := debuginfod.NewClient(debuginfod.Options{
ServerURLs: []string{"https://debuginfod.elfutils.org"},
})
if err != nil {
return err
}
rc, err := client.Fetch(ctx, key.DebugInfo(buildID))
if err != nil {
return err
}
defer rc.Close()The key package exposes constructors for the four artifact kinds: DebugInfo, Executable, Source, and Section. All options besides ServerURLs are optional. See Options for the full list.
If any server returns the artifact, it is returned immediately. Otherwise the library waits for every server to respond and reports one of:
ErrNotFound, if at least one server returned 404 or 410. Treat as a definitive absence.ErrAuthRequired, if no server returned 404 or 410 and at least one returned 401 or 403. Treat as a hint to provide credentials.
5xx and 429 responses propagate as transport errors and are retried. All other 4xx codes are folded into ErrNotFound.
The cache subpackage provides DiskCache, a filesystem-backed cache that wraps a Client (or any value implementing the package's Fetcher interface) and returns *os.File handles suitable for random-access reads, for example ELF parsing via debug/elf.
import (
"go.kacmar.sk/debuginfod"
"go.kacmar.sk/debuginfod/cache"
"go.kacmar.sk/debuginfod/key"
)
userCacheDir, err := os.UserCacheDir()
if err != nil {
return err
}
client, err := debuginfod.NewClient(debuginfod.Options{
ServerURLs: []string{"https://debuginfod.elfutils.org"},
})
if err != nil {
return err
}
disk, err := cache.NewDiskCache(cache.DiskCacheOptions{
Client: client,
Dir: filepath.Join(userCacheDir, "debuginfod"),
})
if err != nil {
return err
}
defer disk.Close()
f, err := disk.Get(ctx, key.DebugInfo(buildID))
if err != nil {
return err
}
defer f.Close()Writes commit atomically via a staging file. The cache is symlink-safe and refuses to follow symlinks pointing outside the cache directory. Get resolves cache hits without invoking the underlying Client. On a miss, the fetched response is streamed to disk and the committed file is returned.
DiskCache does not bound its own size or delete old entries automatically. Use Delete to remove a single entry, or manage the cache directory externally (e.g. a periodic sweep based on file mtime).
Get does not coalesce concurrent requests for the same uncached key. All callers will fetch. Misses are not cached, so every Get for an absent artifact re-runs the federated lookup.
Fetch with a key.Section calls the upstream /section endpoint and returns its response. Not every debuginfod server implements /section, in which case the call returns ErrNotFound and the library does not attempt any further action on its own.
The library never silently escalates a section request into a full debuginfo download, which is a policy decision left to the caller.
If you want a section and the upstream cannot serve it, fetch the full debuginfo and extract the section locally with debug/elf. The cache.DiskCache is well-suited to this pattern since it returns *os.File.
Each retry round fans out across all configured servers in parallel. A body or an authoritative error (ErrNotFound or ErrAuthRequired) from any server ends the round. Transport errors (network failures plus 5xx and 429 responses) trigger another round.
A cache miss requires every configured server to respond, since one server's 404 is not authoritative for the federation. Total latency on a miss is therefore bounded by the slowest server, not the fastest.
Use a context deadline or a per-attempt HTTP transport timeout to cap this (see Timeouts).
backoff, err := debuginfod.ExponentialBackoff(2*time.Second, 60*time.Second)
if err != nil {
return err
}
client, err := debuginfod.NewClient(debuginfod.Options{
ServerURLs: []string{"https://debuginfod.elfutils.org"},
HTTP: debuginfod.HTTPOptions{
MaxRetries: 4,
Backoff: backoff,
},
})When Backoff is unset, the library uses ExponentialBackoff(1s, 30s) (Full Jitter, doubling each round, capped at the max).
For private debuginfod instances, inject a custom http.RoundTripper via HTTPOptions.Client and decorate outbound requests with the auth scheme you need (per-host routing via req.URL.Host). The same hook covers bearer tokens, basic auth, and mTLS (Transport.TLSClientConfig).
For shared or public servers, set HTTPOptions.UserAgent to identify your client to debuginfod operators.
Pass a *slog.Logger via Options.Logger to surface non-fatal events such as retry attempts. When unset, the library logs nothing.
client, err := debuginfod.NewClient(debuginfod.Options{
ServerURLs: []string{"https://debuginfod.elfutils.org"},
Logger: slog.Default(),
})The library does not impose timeouts of its own.
Two knobs cover the common cases:
- Total budget across all servers and retries: set a deadline on the
context.Contextpassed toFetch. On a cache miss, this budget covers the slowest server, not the fastest (see Retries). - Per-attempt budget: configure your own
*http.ClientviaHTTPOptions.Clientand usehttp.Client.Timeout,Transport.ResponseHeaderTimeout, or a customnet.Dialer.Timeout(wired throughTransport.DialContext) to bound how long any single server request may stall.
MIT License - see LICENSE for details.