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,11 +28,17 @@ steps:
|
|||||||
printf '{"auths":{"registry.be.kaien.ru":{"auth":"%s"}}}'
|
printf '{"auths":{"registry.be.kaien.ru":{"auth":"%s"}}}'
|
||||||
"$(printf '%s:%s' "$ZOT_USERNAME" "$ZOT_PASSWORD" | base64 | tr -d '\n')"
|
"$(printf '%s:%s' "$ZOT_USERNAME" "$ZOT_PASSWORD" | base64 | tr -d '\n')"
|
||||||
> /kaniko/.docker/config.json
|
> /kaniko/.docker/config.json
|
||||||
- >-
|
# Stamp buildinfo via Dockerfile ARG/ldflags.
|
||||||
/kaniko/executor
|
# Woodpecker expands ${VAR} before the shell — escape shell locals as $${VAR}.
|
||||||
--context "$CI_WORKSPACE"
|
- |
|
||||||
--dockerfile "$CI_WORKSPACE/Dockerfile"
|
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:$CI_COMMIT_SHA"
|
/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"
|
--destination "registry.be.kaien.ru/l_ai_agent_kaya/agent_kaya:latest"
|
||||||
depends_on:
|
depends_on:
|
||||||
- test
|
- test
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@ -6,7 +6,17 @@ RUN apk add --no-cache git
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
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
|
FROM alpine:3.20
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"l-ai-agent-kaya/internal/buildinfo"
|
||||||
"l-ai-agent-kaya/internal/config"
|
"l-ai-agent-kaya/internal/config"
|
||||||
"l-ai-agent-kaya/internal/inference"
|
"l-ai-agent-kaya/internal/inference"
|
||||||
"l-ai-agent-kaya/internal/profile"
|
"l-ai-agent-kaya/internal/profile"
|
||||||
@ -42,8 +43,11 @@ func run() error {
|
|||||||
}
|
}
|
||||||
defer core.Close()
|
defer core.Close()
|
||||||
|
|
||||||
go serveHTTP(cfg.MetricsListenAddr, promhttp.Handler())
|
ops := http.NewServeMux()
|
||||||
go serveHTTP(cfg.HealthListenAddr, healthHandler())
|
ops.Handle("/metrics", promhttp.Handler())
|
||||||
|
ops.Handle("/healthz", buildinfo.Handler())
|
||||||
|
go serveHTTP(cfg.MetricsListenAddr, ops)
|
||||||
|
go serveHTTP(cfg.HealthListenAddr, buildinfo.Handler())
|
||||||
|
|
||||||
loop := &tasksboard.Loop{
|
loop := &tasksboard.Loop{
|
||||||
WSURL: cfg.TasksWSURL,
|
WSURL: cfg.TasksWSURL,
|
||||||
@ -75,12 +79,3 @@ func serveHTTP(addr string, h http.Handler) {
|
|||||||
slog.Error("http server stopped", "addr", addr, "error", err)
|
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