buildinfo

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

README

buildinfo

Build metadata for Go binaries — version, where that version came from, commit, build time, branch, Go version, OS / arch, dirty flag, and dependency module list — populated automatically from runtime/debug.ReadBuildInfo (Go 1.18+) with -ldflags overrides for CI-stamped builds.

Zero third-party dependencies in the core. HTTP, OTEL, Zap, and slog integrations live in separate adapter modules under contrib/.

How the pieces fit together

buildinfo populates a single Info struct at process start from two sources, then every contrib consumes that struct and surfaces it somewhere — an HTTP route, OTEL resource attributes, Zap fields, slog attrs. There is no registry, no observer pattern, no async work; it's a build-time fact set, exposed many ways.

               ┌────────────────────────────────────────────────┐
               │                YOUR SERVICE                    │
               │                                                │
   -ldflags ─→ │  ┌──────────────────────┐                      │
   runtime    │  │   buildinfo.Get()    │  cached on first call │
   /debug ─→  │  │   ↓                  │                       │
               │  │   Info{Version,      │                       │
               │  │     Commit,          │                       │
               │  │     BuildTime,       │                       │
               │  │     Branch,          │                       │
               │  │     GoVersion,       │                       │
               │  │     GOOS, GOARCH,    │                       │
               │  │     Modified,        │                       │
               │  │     Modules[]}       │                       │
               │  └──────────┬───────────┘                       │
               │             │                                   │
               │             ├────────────────────┐              │
               │             │                    │              │
               │             ▼                    ▼              │
               │  ┌──────────────────┐   ┌──────────────────┐    │
               │  │ HTTP ADAPTERS    │   │ LOGGER + OTEL    │    │
               │  │  buildinfo-      │   │  buildinfo-otel  │    │
               │  │   nethttp / gin /│   │  buildinfo-zap   │    │
               │  │   chi / echo /   │   │  buildinfo-slog  │    │
               │  │   fiber          │   │                  │    │
               │  └────────┬─────────┘   └────────┬─────────┘    │
               │           │ /version JSON        │ Attrs/Fields │
               └───────────┼──────────────────────┼──────────────┘
                           ▼                      ▼
                      curl / k8s          attached to every span,
                      release dashboard    metric, and log line

Every adapter is read-only against buildinfo.Info. None of them perform any I/O on their own; they just make the same struct available in different output formats.

Install

go get github.com/ubgo/buildinfo

Quick start

package main

import (
    "log"

    "github.com/ubgo/buildinfo"
)

func main() {
    info := buildinfo.Get()
    log.Printf("starting %s commit=%s go=%s",
        info.Version, info.Commit, info.GoVersion)
}

Without any build configuration, you'll see something like:

starting dev commit=abcdef0 go=go1.24.0

Commit was filled by runtime/debug.ReadBuildInfo reading the VCS metadata Go embeds since 1.18. Version reads Main.Version when the binary was resolved through the module proxy (see below); a locally built binary has no module version, so it falls back to "dev" until you stamp it via -ldflags.

CI version stamping

Stamp release values at build time via -ldflags:

go build -ldflags="\
  -X github.com/ubgo/buildinfo.Version=$(git describe --tags --always) \
  -X github.com/ubgo/buildinfo.Commit=$(git rev-parse HEAD) \
  -X github.com/ubgo/buildinfo.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
  -X github.com/ubgo/buildinfo.Branch=$(git rev-parse --abbrev-ref HEAD)"

-ldflags overrides win over runtime/debug data.

go install versioning

When a user installs your binary straight from the module proxy, the Go toolchain records the resolved module version in Main.Version — no build flags, no CI:

go install example.com/cmd/app@v1.2.3
app version   # -> v1.2.3

buildinfo reads that automatically, so go install-ed binaries report a real version instead of "dev". This matters most for CLIs, where go install is a normal distribution channel; services are usually built by CI and stamped via -ldflags.

Binaries built from a local source tree (go build, go run, go test) have no module version — the toolchain reports the placeholder (devel), which buildinfo treats as absent so the "dev" sentinel applies.

Precedence, highest first:

  1. -ldflags overrides.
  2. runtime/debug.ReadBuildInfoMain.Version for the version, vcs.* settings for commit / time / dirty flag.
  3. Sentinel defaults (buildinfo.DevVersion, buildinfo.Unknown).

Provenance — which input won

The version string on its own is ambiguous. A binary reports dev both when someone built it locally and when a release pipeline silently failed to pass its ldflags. Those need very different responses, so Info.Source records which input actually produced Info.Version:

Source Meaning
SourceLdflags stamped at build time by the release pipeline
SourceModule resolved by go install <pkg>@<version>
SourceUnknown plain go build / go run / go testVersion is the DevVersion placeholder
info := buildinfo.Get()
log.Printf("starting %s (%s)", info.Version, info.Source)

if !info.HasVersion() {
    log.Println("warning: unstamped build")
}

Two helpers express the same distinction as booleans, so you never compare against the sentinel strings yourself:

  • info.HasVersion()Version came from a real input (equivalent to Source != SourceUnknown).
  • info.HasCommit()Commit is a real hash rather than Unknown. Worth checking before rendering Modified: unknown (dirty) asserts a dirty checkout for a build carrying no VCS record at all.

The sentinels are exported as buildinfo.DevVersion and buildinfo.Unknown. Compare against those rather than hardcoding "dev" / "unknown" — a duplicated sentinel in your own code breaks silently if this package ever changes one.

API

// Auto-populated, cached after first call.
info := buildinfo.Get()

// Flat string-only map for simple renderers.
m := buildinfo.Map()

// JSON-marshalled bytes for HTTP / log payloads.
b, _ := info.JSON()

Info fields:

Field Source Default if empty
Version -ldflagsMain.Version DevVersion ("dev")
Source which input produced Version SourceUnknown
Commit -ldflagsvcs.revision Unknown ("unknown")
BuildTime -ldflagsvcs.time Unknown
Branch -ldflags only Unknown
GoVersion runtime.Version()
GOOS runtime.GOOS
GOARCH runtime.GOARCH
Modified vcs.modified false
Modules runtime/debug.BuildInfo.Deps empty slice

Why not just runtime/debug.ReadBuildInfo directly?

runtime/debug.ReadBuildInfo() gives you a *debug.BuildInfo with Main.Version, Settings (a slice of key-value pairs you have to scan), and Deps — accurate, but inconvenient. Every project that exposes build metadata ends up writing the same parsing layer.

buildinfo is that layer, plus three things stdlib doesn't give you:

stdlib buildinfo
Read VCS commit / time / dirty flag scan Settings []KeyValue for vcs.revision / vcs.time / vcs.modified already extracted into Info.Commit / BuildTime / Modified
Read the module version from go install read Main.Version, then filter the (devel) placeholder yourself already extracted into Info.Version
Know WHICH input produced the version track it yourself across every branch Info.Source + HasVersion() / HasCommit()
Branch field not available — Go doesn't capture the current branch populated via -ldflags
-ldflags overrides for CI-stamped versions DIY -X github.com/ubgo/buildinfo.Version=… works out of the box
JSON / map output DIY info.JSON(), buildinfo.Map()
HTTP / OTEL / Zap / slog plumbing DIY for each one contrib import per integration

If you only need version + commit and don't mind the boilerplate, stdlib is fine. If you're going to expose /version, attach build attrs to OTEL resources, and tag log lines, the per-place plumbing adds up — that's the value of contribs.

Composing multiple adapters

The Info struct is read-only and the same value across every contrib. Use as many as you need without coordination:

import (
    "log/slog"
    "net/http"
    "os"

    binethttp "github.com/ubgo/buildinfo/contrib/buildinfo-nethttp"
    biotel    "github.com/ubgo/buildinfo/contrib/buildinfo-otel"
    bislog    "github.com/ubgo/buildinfo/contrib/buildinfo-slog"

    sdkresource "go.opentelemetry.io/otel/sdk/resource"
    sdktrace    "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
    // 1. Expose /version via HTTP.
    mux := http.NewServeMux()
    binethttp.Mount(mux)

    // 2. Tag every slog line with a "build" group.
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
        With(bislog.Group())
    slog.SetDefault(logger)

    // 3. Attach build metadata as OpenTelemetry resource attributes,
    //    so every span / metric carries it.
    res, _ := sdkresource.Merge(
        sdkresource.Default(),
        sdkresource.NewSchemaless(biotel.Attrs()...),
    )
    tp := sdktrace.NewTracerProvider(sdktrace.WithResource(res))
    _ = tp // wire to global TracerProvider, etc.

    _ = http.ListenAndServe(":8080", mux)
}

buildinfo.Get() is called once on first access and cached — composing N adapters does not multiply the cost.

Adapters

Adapter modules ship as separate Go modules under contrib/. Import only the ones you use; each pulls in its own dependencies.

Adapter Module path Purpose
buildinfo-nethttp github.com/ubgo/buildinfo/contrib/buildinfo-nethttp stdlib /version handler + Mount
buildinfo-gin github.com/ubgo/buildinfo/contrib/buildinfo-gin Gin /version handler + Mount
buildinfo-chi github.com/ubgo/buildinfo/contrib/buildinfo-chi Chi /version Mount helper
buildinfo-echo github.com/ubgo/buildinfo/contrib/buildinfo-echo Echo /version handler + Mount
buildinfo-fiber github.com/ubgo/buildinfo/contrib/buildinfo-fiber Fiber /version handler + Mount
buildinfo-otel github.com/ubgo/buildinfo/contrib/buildinfo-otel OpenTelemetry resource attributes
buildinfo-zap github.com/ubgo/buildinfo/contrib/buildinfo-zap Zap log fields (Fields + Namespace)
buildinfo-slog github.com/ubgo/buildinfo/contrib/buildinfo-slog stdlib slog Attrs (Attrs + Group)

Click any adapter for its dedicated README with install, quick start, middleware, and API tables.

All eight adapters ship in v0.1.0. Each is a separate Go module under contrib/<adapter>/ and pulls only its own dependencies.

Comparison

Feature stdlib runtime/debug carlmjohnson/versioninfo ubgo/buildinfo
Reads vcs.revision / vcs.time / vcs.modified manual scan of Settings ✅ (extracted to typed fields)
Branch field via -ldflags
Cached Get() (read once at boot) ❌ — re-parses each call
HTTP /version adapters ✅ (5 frameworks)
OpenTelemetry resource attributes
Zap / slog field helpers
Module list (SBOM-friendly) ✅ (Deps)
Zero-dep core

Compatibility

Requires Go 1.24 or later.

License

Apache License 2.0. See LICENSE and NOTICE.

Documentation

Overview

Package buildinfo exposes build-time and runtime metadata for a Go binary — version, its provenance, commit, build time, branch, Go version, OS/arch, dirty flag, and the list of dependency modules.

Values are populated from two inputs, in priority order:

  1. -ldflags overrides set at build time (highest precedence).
  2. runtime/debug.ReadBuildInfo (Go 1.18+) — Main.Version for the module version recorded by `go install pkg@version`, plus VCS data for the commit, build time, and dirty flag.

When neither applies, fields fall back to the DevVersion and Unknown sentinels so callers can render them without nil checks.

Provenance

Info.Source reports which input produced Info.Version — SourceLdflags, SourceModule, or SourceUnknown. This matters because the version string alone is ambiguous: a binary reports "dev" both when it was built locally and when a release pipeline failed to pass its ldflags. Displaying the source turns "why does it say dev?" into a self-answering question.

Info.HasCommit and Info.HasVersion express the same distinction as booleans, so callers never compare against the sentinel strings themselves.

Dependencies

The package has zero third-party dependencies. HTTP, OTEL, Zap, and slog integrations live in separate adapter modules under contrib/, so importing this package pulls in nothing but the standard library.

Typical use

info := buildinfo.Get()
log.Printf("starting %s (%s) commit=%s", info.Version, info.Source, info.Commit)

if !info.HasVersion() {
    log.Println("warning: unstamped build")
}

Build with version stamping

go build -ldflags="-X github.com/ubgo/buildinfo.Version=1.2.3"

The -X target is this package's path rather than the consumer's main package, so the same flags work unchanged in any project.

Index

Constants

View Source
const (
	// DevVersion is Info.Version when no -ldflags stamp and no module version
	// were available, i.e. a plain local build.
	DevVersion = "dev"

	// Unknown is the fallback for Commit, BuildTime, and Branch.
	Unknown = "unknown"
)

Sentinel values used when an input supplied nothing. Exported so callers can compare against them instead of hardcoding the literals — a duplicated sentinel in a consumer breaks silently if this package ever changes one.

Variables

View Source
var (
	// Version is the semver string for this build (e.g. "1.2.3").
	Version string

	// Commit is the VCS commit hash.
	Commit string

	// BuildTime is the build timestamp in RFC3339 format.
	BuildTime string

	// Branch is the VCS branch name.
	Branch string
)

These variables can be overridden via -ldflags at build time:

-ldflags="-X github.com/ubgo/buildinfo.Version=1.2.3 \
          -X github.com/ubgo/buildinfo.Commit=abc123 \
          -X github.com/ubgo/buildinfo.BuildTime=2026-04-26T12:00:00Z \
          -X github.com/ubgo/buildinfo.Branch=main"

When unset, values fall back to runtime/debug.ReadBuildInfo (Go 1.18+ VCS data) where applicable. ldflags overrides always win.

Functions

func Map

func Map() map[string]string

Map returns Info as a flat string-only map for legacy or string-typed consumers (e.g. simple key-value renderers).

Modified and Modules are omitted: the former is not a string and the latter is unbounded, which would swamp a log line or a metric label set.

Types

type Info

type Info struct {
	// Version is the semver string for this build, or DevVersion when nothing
	// supplied one. Check Source to find out which.
	Version string `json:"version"`

	// Source records which input produced Version. See Source.
	Source Source `json:"source"`

	// Commit is the VCS commit hash, or Unknown.
	Commit string `json:"commit"`

	// BuildTime is the build timestamp in RFC3339 format, or Unknown.
	BuildTime string `json:"build_time"`

	// Branch is the VCS branch name, or Unknown. Go records no branch, so this
	// is populated only via -ldflags.
	Branch string `json:"branch"`

	// GoVersion is the toolchain version that produced the binary.
	GoVersion string `json:"go_version"`

	// GOOS is the target operating system.
	GOOS string `json:"goos"`

	// GOARCH is the target architecture.
	GOARCH string `json:"goarch"`

	// Modified reports an uncommitted working tree at build time. Meaningful
	// only alongside a real commit — see HasCommit.
	Modified bool `json:"modified"`

	// Modules lists dependency modules, with any replace directives resolved
	// to their targets.
	Modules []Module `json:"modules,omitempty"`
}

Info contains build metadata for a Go binary.

String fields default to DevVersion (Version) or Unknown (Commit, BuildTime, Branch) when neither -ldflags nor runtime/debug data populate them, so callers can render them safely without nil checks. Because those defaults are indistinguishable from real values by inspection alone, Source records which input actually won and HasCommit / HasVersion report whether a field holds real data.

func Get

func Get() Info

Get returns the populated Info struct, cached after the first call.

Population precedence (highest first):

  1. -ldflags overrides set at build time.
  2. runtime/debug.ReadBuildInfo data — the module version recorded in Main.Version, plus VCS data (vcs.revision, vcs.time, vcs.modified).
  3. Sentinel defaults (DevVersion for Version, Unknown for Commit / BuildTime / Branch).

Info.Source reports which of those produced Version, so a caller never has to infer provenance from the string itself.

func (Info) HasCommit added in v0.1.2

func (i Info) HasCommit() bool

HasCommit reports whether Commit holds a real hash rather than the Unknown sentinel.

Needed because Modified is only meaningful alongside a commit: rendering "unknown (dirty)" asserts a dirty checkout for a build that carries no VCS record at all. Prefer this over comparing against Unknown yourself — the sentinel is this package's business, not its callers'.

func (Info) HasVersion added in v0.1.2

func (i Info) HasVersion() bool

HasVersion reports whether Version came from a real input (an -ldflags stamp or a resolved module version) rather than falling back to DevVersion.

Equivalent to Source != SourceUnknown, and offered because that is the check most callers actually want when deciding whether to display a version at all.

func (Info) JSON

func (i Info) JSON() ([]byte, error)

JSON returns the Info marshalled as JSON bytes.

type Module

type Module struct {
	Path    string `json:"path"`
	Version string `json:"version"`
	Sum     string `json:"sum,omitempty"`
}

Module describes a single dependency module entry from runtime/debug.

type Source added in v0.1.2

type Source string

Source identifies which input produced Info.Version.

Exists because the version string alone cannot answer the most common question a user asks about a build: "why does it say dev?" A binary reports DevVersion both when it was built locally and when a release pipeline forgot to pass its ldflags, and those need different responses. Surfacing the source makes the difference self-diagnosing rather than a support conversation.

const (
	// SourceLdflags means Version was stamped at build time via
	// -X github.com/ubgo/buildinfo.Version=... — the release path.
	SourceLdflags Source = "ldflags"

	// SourceModule means Version came from the module version the Go toolchain
	// recorded in BuildInfo.Main.Version, i.e. the binary was installed with
	// `go install <pkg>@<version>`.
	SourceModule Source = "module"

	// SourceUnknown means neither input applied and Version is DevVersion: a
	// plain `go build`, `go run`, or `go test` binary with no release
	// provenance.
	SourceUnknown Source = "unknown"
)

func (Source) String added in v0.1.2

func (s Source) String() string

String implements fmt.Stringer.

func (Source) Valid added in v0.1.2

func (s Source) Valid() bool

Valid reports whether s is a known source. Guards against a zero-value or hand-constructed Info being treated as if it carried real provenance.

Directories

Path Synopsis
contrib
buildinfo-chi module
buildinfo-gin module
buildinfo-zap module

Jump to

Keyboard shortcuts

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