Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9397efe
feat: Add build information
PythonGermany Dec 13, 2025
3b14c65
refactor: Rename buildinfo package
PythonGermany Dec 13, 2025
db16de0
refactor: Make build info names consistent
PythonGermany Dec 13, 2025
42d6c32
feat(metrics): Expose build information
PythonGermany Dec 13, 2025
4834898
docs: Add build info
PythonGermany Dec 13, 2025
a97547d
fix: Only set BuildVersion if showing it in the UI is enabled
PythonGermany Dec 13, 2025
fca1c0e
test(buildinfo): Add default test
PythonGermany Dec 13, 2025
5419779
fix: Hide tag not found error output
PythonGermany Dec 13, 2025
5e45497
ci: Use tag as version build arg
PythonGermany Dec 13, 2025
a11ca1a
chore: Remove release link to keep it simple
PythonGermany Dec 14, 2025
15d1733
chore(ui): Regenerate static assets
PythonGermany Dec 14, 2025
73d4c3d
feat: Use embedded build info
PythonGermany Dec 15, 2025
a7126d6
fix: Get commit timestamp correctly
PythonGermany Dec 15, 2025
3a41a74
refactor: Rename buildinfo date to revision date
PythonGermany Dec 15, 2025
e802f99
docs: Add comment explaining special default case
PythonGermany Dec 17, 2025
a2a657b
Merge remote-tracking branch 'TwiN/master' into add-build-information
PythonGermany Dec 18, 2025
0bc9375
refactor(ui): Rename ShowBuildInfo to ShowVersion
PythonGermany Dec 18, 2025
12e2601
chore(ui): Change ShowVersion default to false
PythonGermany Dec 18, 2025
4743298
test: Add missing config parameters
PythonGermany Dec 18, 2025
3776343
fix(docker): Outdated parameter name
PythonGermany Dec 18, 2025
12d5d3a
refactor(ui): Simplify show version condition
PythonGermany Dec 18, 2025
c62b2d5
Merge remote-tracking branch 'TwiN/master' into add-build-information
PythonGermany Dec 19, 2025
5843575
chore(ui): Regenerate static assets
PythonGermany Dec 19, 2025
2291d8d
Merge branch 'master' into add-build-information
TwiN Dec 22, 2025
9b96014
Merge remote-tracking branch 'TwiN/master' into add-build-information
PythonGermany Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/publish-custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
inputs:
tag:
description: Custom tag to publish
default: custom
platforms:
description: Platforms to publish to (comma separated list)
default: linux/amd64
Expand Down Expand Up @@ -39,6 +40,8 @@ jobs:
images: ${{ env.GHCR_IMAGE_REPOSITORY }}
tags: |
type=raw,value=${{ inputs.tag }}
- name: Get commit timestamp
run: echo "COMMIT_TIMESTAMP=$(TZ=UTC git show -s --format=%cd --date=iso-strict-local)" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
Expand All @@ -47,3 +50,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ inputs.tag }}
REVISION=${{ github.sha }}
REVISION_DATE=${{ env.COMMIT_TIMESTAMP }}
6 changes: 6 additions & 0 deletions .github/workflows/publish-experimental.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
images: ${{ env.IMAGE_REPOSITORY }}
tags: |
type=raw,value=experimental
- name: Get commit timestamp
run: echo "COMMIT_TIMESTAMP=$(TZ=UTC git show -s --format=%cd --date=iso-strict-local)" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
Expand All @@ -32,3 +34,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=experimental
REVISION=${{ github.sha }}
REVISION_DATE=${{ env.COMMIT_TIMESTAMP }}
6 changes: 6 additions & 0 deletions .github/workflows/publish-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
${{ env.GHCR_IMAGE_REPOSITORY }}
tags: |
type=raw,value=latest
- name: Get commit timestamp
run: echo "COMMIT_TIMESTAMP=$(TZ=UTC git show -s --format=%cd --date=iso-strict-local)" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
Expand All @@ -50,3 +52,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=latest
REVISION=${{ github.sha }}
REVISION_DATE=${{ env.COMMIT_TIMESTAMP }}
6 changes: 6 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
type=raw,value=${{ env.RELEASE }}
type=raw,value=stable
type=raw,value=latest
- name: Get commit timestamp
run: echo "COMMIT_TIMESTAMP=$(TZ=UTC git show -s --format=%cd --date=iso-strict-local)" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
Expand All @@ -49,3 +51,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ env.RELEASE }}
REVISION=${{ github.sha }}
REVISION_DATE=${{ env.COMMIT_TIMESTAMP }}
11 changes: 10 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
# Build the go application into a binary
FROM golang:alpine AS builder

ARG VERSION
ARG REVISION
ARG REVISION_DATE

RUN apk --update add ca-certificates
WORKDIR /app
COPY . ./
RUN go mod tidy -diff
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gatus .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
-ldflags "-X github.com/TwiN/gatus/v5/buildinfo.version=${VERSION} \
-X github.com/TwiN/gatus/v5/buildinfo.revision=${REVISION} \
-X github.com/TwiN/gatus/v5/buildinfo.date=${REVISION_DATE}" \
-o gatus .

# Run Tests inside docker image if you don't have a configured go environment
#RUN apk update && apk add --virtual build-dependencies build-base gcc
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
BINARY=gatus
VERSION=$(shell git describe --tags --exact-match 2> /dev/null)

.PHONY: install
install:
go build -v -o $(BINARY) .
go build -v -ldflags "-X github.com/TwiN/gatus/v5/buildinfo.version=$(VERSION)" -o $(BINARY) .

.PHONY: run
run:
Expand All @@ -25,8 +26,12 @@ test:
# Docker #
##########

DIRTY=$(shell test -n "$$(git status --porcelain)" && echo "-dirty")
docker-build:
docker build -t twinproduction/gatus:latest .
docker build --build-arg VERSION=$(VERSION) \
--build-arg REVISION=$(shell git rev-parse HEAD)$(DIRTY) \
--build-arg REVISION_DATE=$(shell TZ=UTC git show -s --format=%cd --date=iso-strict-local) \
-t twinproduction/gatus:latest .

docker-run:
docker run -p 8080:8080 --name gatus twinproduction/gatus:latest
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ If you want to test it locally, see [Docker](#docker).
| `ui.dark-mode` | Whether to enable dark mode by default. Note that this is superseded by the user's operating system theme preferences. | `true` |
| `ui.default-sort-by` | Default sorting option for endpoints in the dashboard. Can be `name`, `group`, or `health`. Note that user preferences override this. | `name` |
| `ui.default-filter-by` | Default filter option for endpoints in the dashboard. Can be `none`, `failing`, or `unstable`. Note that user preferences override this. | `none` |
| `ui.show-build-info` | Whether to show build information (version in footer). | `true` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` |

If you want more verbose logging, you may set the `GATUS_LOG_LEVEL` environment variable to `DEBUG`.
Expand Down Expand Up @@ -2685,6 +2686,7 @@ endpoint on the same port your application is configured to run on (`web.port`).

| Metric name | Type | Description | Labels | Relevant endpoint types |
|:---------------------------------------------|:--------|:---------------------------------------------------------------------------|:--------------------------------|:------------------------|
| gatus_build_info | gauge | Build information about the instance | version, revision, build_date | None |
| gatus_results_total | counter | Number of results per endpoint per success state | key, group, name, type, success | All |
| gatus_results_code_total | counter | Total number of results by code | key, group, name, type, code | DNS, HTTP |
| gatus_results_connected_total | counter | Total number of results in which a connection was successfully established | key, group, name, type | All |
Expand Down
84 changes: 84 additions & 0 deletions buildinfo/buildinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package buildinfo

import (
"runtime"
"runtime/debug"
)

const (
defaultVersion = "development"
defaultRevision = "unknown"
defaultRevisionDate = "unknown"
)

var (
version string
revision string
revisionDate string

buildInfo = SetBuildInfo(GetDefault())
)

type BuildInfo struct {
Version string
Revision string
RevisionDate string
GoVersion string
Dirty bool
}

func SetEmbedded(info *BuildInfo) {
rawInfo, ok := debug.ReadBuildInfo()
if !ok {
info.Revision += "-buildinfo-err"
}
for _, s := range rawInfo.Settings {
switch s.Key {
case "vcs.revision":
info.Revision = s.Value
case "vcs.time":
info.RevisionDate = s.Value
case "vcs.modified":
info.Dirty = s.Value == "true"
}
}
}

func SetLdflags(info *BuildInfo) {
if len(version) > 0 {
info.Version = version
}
if len(revision) > 0 {
info.Revision = revision
}
if len(revisionDate) > 0 {
info.RevisionDate = revisionDate
}
if len(info.GoVersion) == 0 {
info.GoVersion = runtime.Version()
}
}

func GetDefault() BuildInfo {
return BuildInfo{
Version: defaultVersion,
Revision: defaultRevision,
RevisionDate: defaultRevisionDate,
GoVersion: runtime.Version(),
Dirty: false,
}
}

func SetBuildInfo(info BuildInfo) BuildInfo {
SetEmbedded(&info)
SetLdflags(&info)

if info.Dirty {
info.Revision += "-dirty"
}
return info
}

func Get() BuildInfo {
return buildInfo
}
167 changes: 167 additions & 0 deletions buildinfo/buildinfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package buildinfo

import (
"runtime"
"testing"
)

func TestBuildInfo_GetDefaults(t *testing.T) {
info := GetDefault()
if info.Version != defaultVersion {
t.Errorf("Expected default Version '%s', got '%s'", defaultVersion, info.Version)
}
if info.Revision != defaultRevision {
t.Errorf("Expected default Revision '%s', got '%s'", defaultRevision, info.Revision)
}
if info.RevisionDate != defaultRevisionDate {
t.Errorf("Expected default RevisionDate '%s', got '%s'", defaultRevisionDate, info.RevisionDate)
}
if info.GoVersion != runtime.Version() {
t.Errorf("Expected GoVersion '%s', got '%s'", runtime.Version(), info.GoVersion)
}
if info.Dirty != false {
t.Errorf("Expected Dirty 'false', got '%v'", info.Dirty)
}
}

func TestBuildInfo_SetLdflags(t *testing.T) {
t.Run("NoLdflags", func(t *testing.T) {
info := GetDefault()
SetLdflags(&info)

// Since we are not setting ldflags during the test, the values largely remain defaults
if info.Version != defaultVersion {
t.Errorf("Expected Version '%s', got '%s'", defaultVersion, info.Version)
}
if info.Revision != defaultRevision {
t.Errorf("Expected Revision '%s', got '%s'", defaultRevision, info.Revision)
}
if info.RevisionDate != defaultRevisionDate {
t.Errorf("Expected RevisionDate '%s', got '%s'", defaultRevisionDate, info.RevisionDate)
}
if info.GoVersion != runtime.Version() {
t.Errorf("Expected GoVersion '%s', got '%s'", runtime.Version(), info.GoVersion)
}
if info.Dirty != false {
t.Errorf("Expected Dirty 'false', got '%v'", info.Dirty)
}
})
t.Run("WithLdflags", func(t *testing.T) {
// Simulate ldflags being set
originalVersion := version
originalRevision := revision
originalRevisionDate := revisionDate
defer func() {
version = originalVersion
revision = originalRevision
revisionDate = originalRevisionDate
}()
version = "test-version"
revision = "test-revision"
revisionDate = "test-date"

info := GetDefault()
SetLdflags(&info)

if info.Version != "test-version" {
t.Errorf("Expected Version 'test-version', got '%s'", info.Version)
}
if info.Revision != "test-revision" {
t.Errorf("Expected Revision 'test-revision', got '%s'", info.Revision)
}
if info.RevisionDate != "test-date" {
t.Errorf("Expected RevisionDate ''test-date', got '%s'", info.RevisionDate)
}
if info.GoVersion != runtime.Version() {
t.Errorf("Expected GoVersion '%s', got '%s'", runtime.Version(), info.GoVersion)
}
if info.Dirty != false {
t.Errorf("Expected Dirty 'false', got '%v'", info.Dirty)
}
})
}

func TestBuildInfo_SetEmbedded(t *testing.T) {
info := GetDefault()
SetEmbedded(&info)

if info.Version != defaultVersion {
t.Errorf("Expected Version '%s', got '%s'", defaultVersion, info.Version)
}
if info.Revision != defaultRevision {
t.Errorf("Expected Revision '%s', got '%s'", defaultRevision, info.Revision)
}
if info.RevisionDate != defaultRevisionDate {
t.Errorf("Expected RevisionDate '%s', got '%s'", defaultRevisionDate, info.RevisionDate)
}
if info.GoVersion != runtime.Version() {
t.Errorf("Expected GoVersion '%s', got '%s'", runtime.Version(), info.GoVersion)
}
if info.Dirty != false {
t.Errorf("Expected Dirty 'false', got '%v'", info.Dirty)
}
}

func TestBuildInfo_SetBuildInfo(t *testing.T) {
t.Run("TestEmbeddOverwriteLdflags", func(t *testing.T) {
// Only change one ldflag at a time to test their effects
originalVersion := version
defer func() {
version = originalVersion
}()
version = "test-version"

defaultInfo := GetDefault()
info := SetBuildInfo(defaultInfo)
if info.Version != "test-version" {
t.Errorf("Expected Version 'test-version', got '%s'", info.Version)
}
if info.Revision != defaultRevision {
t.Errorf("Expected Revision '%s', got '%s'", defaultRevision, info.Revision)
}
if info.RevisionDate != defaultRevisionDate {
t.Errorf("Expected RevisionDate '%s', got '%s'", defaultRevisionDate, info.RevisionDate)
}
if info.GoVersion != runtime.Version() {
t.Errorf("Expected GoVersion '%s', got '%s'", runtime.Version(), info.GoVersion)
}
if info.Dirty != false {
t.Errorf("Expected Dirty 'false', got '%v'", info.Dirty)
}

// Change another ldflag
originalRevision := revision
defer func() {
revision = originalRevision
}()
revision = "test-rev"

info = SetBuildInfo(GetDefault())
if info.Revision != "test-rev" {
t.Errorf("Expected Revision 'test-rev', got '%s'", info.Revision)
}

// Change the last ldflag
originalRevisionDate := revisionDate
defer func() {
revisionDate = originalRevisionDate
}()
revisionDate = "test-date"

info = SetBuildInfo(defaultInfo)
if info.RevisionDate != "test-date" {
t.Errorf("Expected RevisionDate 'test-date', got '%s'", info.RevisionDate)
}
})
t.Run("TestDirtyFlag", func(t *testing.T) {
info := GetDefault()
info.Dirty = true // Simulate the embedded info setting Dirty to true
info = SetBuildInfo(info)
if info.Dirty != true {
t.Errorf("Expected Dirty 'true', got '%v'", info.Dirty)
}
if info.Revision != defaultRevision+"-dirty" {
t.Errorf("Expected Revision '%s-dirty', got '%s'", defaultRevision, info.Revision)
}
})
}
Loading