Expose stamped /healthz JSON next to Prometheus metrics.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3a2a0c1c34
commit
7758c097af
@ -28,12 +28,18 @@ steps:
|
||||
printf '{"auths":{"registry.be.kaien.ru":{"auth":"%s"}}}'
|
||||
"$(printf '%s:%s' "$ZOT_USERNAME" "$ZOT_PASSWORD" | base64 | tr -d '\n')"
|
||||
> /kaniko/.docker/config.json
|
||||
- >-
|
||||
/kaniko/executor
|
||||
--context "$CI_WORKSPACE"
|
||||
--dockerfile "$CI_WORKSPACE/Dockerfile"
|
||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA"
|
||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:latest"
|
||||
# Stamp buildinfo via Dockerfile ARG/ldflags.
|
||||
# Woodpecker expands ${VAR} before the shell — escape shell locals as $${VAR}.
|
||||
- |
|
||||
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
/kaniko/executor \
|
||||
--context "$CI_WORKSPACE" \
|
||||
--dockerfile "$CI_WORKSPACE/Dockerfile" \
|
||||
--build-arg "BUILD_DATE=$${BUILD_DATE}" \
|
||||
--build-arg "BUILD_COMMIT=${CI_COMMIT_SHA}" \
|
||||
--build-arg "BUILD_BRANCH=${CI_COMMIT_BRANCH}" \
|
||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA" \
|
||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:latest"
|
||||
depends_on:
|
||||
- test
|
||||
when:
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@ -6,7 +6,17 @@ RUN apk add --no-cache git
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /kaya ./cmd/kaya
|
||||
|
||||
ARG BUILD_DATE=unknown
|
||||
ARG BUILD_COMMIT=dev
|
||||
ARG BUILD_BRANCH=unknown
|
||||
|
||||
RUN CGO_ENABLED=0 go build \
|
||||
-ldflags="-s -w \
|
||||
-X l-ai-agent-kaya/internal/buildinfo.Date=${BUILD_DATE} \
|
||||
-X l-ai-agent-kaya/internal/buildinfo.Commit=${BUILD_COMMIT} \
|
||||
-X l-ai-agent-kaya/internal/buildinfo.Branch=${BUILD_BRANCH}" \
|
||||
-o /kaya ./cmd/kaya
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"l-ai-agent-kaya/internal/buildinfo"
|
||||
"l-ai-agent-kaya/internal/config"
|
||||
"l-ai-agent-kaya/internal/inference"
|
||||
"l-ai-agent-kaya/internal/profile"
|
||||
@ -42,8 +43,11 @@ func run() error {
|
||||
}
|
||||
defer core.Close()
|
||||
|
||||
go serveHTTP(cfg.MetricsListenAddr, promhttp.Handler())
|
||||
go serveHTTP(cfg.HealthListenAddr, healthHandler())
|
||||
ops := http.NewServeMux()
|
||||
ops.Handle("/metrics", promhttp.Handler())
|
||||
ops.Handle("/healthz", buildinfo.Handler())
|
||||
go serveHTTP(cfg.MetricsListenAddr, ops)
|
||||
go serveHTTP(cfg.HealthListenAddr, buildinfo.Handler())
|
||||
|
||||
loop := &tasksboard.Loop{
|
||||
WSURL: cfg.TasksWSURL,
|
||||
@ -75,12 +79,3 @@ func serveHTTP(addr string, h http.Handler) {
|
||||
slog.Error("http server stopped", "addr", addr, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func healthHandler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
96
internal/buildinfo/buildinfo.go
Normal file
96
internal/buildinfo/buildinfo.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Package buildinfo exposes stamped build metadata and the shared ops /healthz JSON.
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
// Populated at link time via -ldflags -X.
|
||||
var (
|
||||
Date = "unknown"
|
||||
Commit = "dev"
|
||||
Branch = "unknown"
|
||||
)
|
||||
|
||||
// Hardcoded service identity for autodiscovery / status dashboard.
|
||||
const (
|
||||
Name = "l-ai-agent-kaya"
|
||||
Description = "Tasks board agent Kaya: @kaya mention replies via core RunAgent"
|
||||
)
|
||||
|
||||
// Tags is the controlled vocabulary for this service (see l-ai-docs services/healthz.md).
|
||||
var Tags = []string{"agent", "tasks", "llm"}
|
||||
|
||||
var startedAt = time.Now()
|
||||
|
||||
func init() {
|
||||
promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "build_info",
|
||||
Help: "Build metadata for this process (always 1).",
|
||||
},
|
||||
[]string{"name", "commit", "branch", "date"},
|
||||
).With(prometheus.Labels{
|
||||
"name": Name,
|
||||
"commit": Commit,
|
||||
"branch": Branch,
|
||||
"date": Date,
|
||||
}).Set(1)
|
||||
}
|
||||
|
||||
// Build is the nested build object in /healthz.
|
||||
type Build struct {
|
||||
Date string `json:"date"`
|
||||
Commit string `json:"commit"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
// Response is the shared /healthz JSON contract.
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
Build Build `json:"build"`
|
||||
}
|
||||
|
||||
// Snapshot returns the current health payload with status "ok".
|
||||
func Snapshot() Response {
|
||||
tags := make([]string, len(Tags))
|
||||
copy(tags, Tags)
|
||||
return Response{
|
||||
Status: "ok",
|
||||
Name: Name,
|
||||
Description: Description,
|
||||
Tags: tags,
|
||||
UptimeSeconds: int64(time.Since(startedAt).Seconds()),
|
||||
Build: Build{
|
||||
Date: Date,
|
||||
Commit: Commit,
|
||||
Branch: Branch,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Handler serves GET/HEAD /healthz as application/json (always 200 while process is up).
|
||||
func Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, Snapshot())
|
||||
})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, code int, body Response) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
42
internal/buildinfo/buildinfo_test.go
Normal file
42
internal/buildinfo/buildinfo_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package buildinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandlerOK(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
Handler().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("code=%d", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Fatalf("content-type=%q", ct)
|
||||
}
|
||||
var body Response
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Status != "ok" || body.Name != Name || body.Description == "" {
|
||||
t.Fatalf("unexpected body: %+v", body)
|
||||
}
|
||||
if len(body.Tags) == 0 {
|
||||
t.Fatal("expected tags")
|
||||
}
|
||||
if body.Build.Commit == "" {
|
||||
t.Fatal("expected commit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerMethodNotAllowed(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
Handler().ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("code=%d", rec.Code)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user