mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
Two endpoints were answering with the right status code and an empty
truth, so every fleet looked instrumented while reporting nothing.
GET /v1/health encoded apihealth.APIReply through jsonv2, which has no
default representation for time.Duration — and every Result carries one.
The marshal therefore failed on every node, every time, and the handler
fell back to {"healthy":…,"error":"health reply encode failed"}. The
status code is written before the encode, so k8s probes and dashboards
stayed green while the body carried no checks at all. Measured on Zoo,
Hanzo and Pars mainnet, node v1.34.9 and v1.36.2 alike.
The type is defined with encoding/json tags and the POST (jsonrpc) path
already encodes it through that codec, which is why POST returned the
full check set while GET returned nothing. One wire type deserves one
encoder: GET now uses encoding/json too, so the two paths agree by
construction. encoding/json also maps invalid UTF-8 in check Details to
U+FFFD instead of failing, which is the behaviour the old jsontext
AllowInvalidUTF8 option was reaching for. The buffer stays: it keeps a
partial encode from shipping a torn body.
/v1/metrics answered 200 with zero bytes for the same shape of reason.
luxfi/metric resolves NewRegistry() to a no-op registry unless the
binary is built with -tags metrics (registry_noop.go, //go:build
!metrics), and only the `full` profile passed it. Images build with the
default `minimal` profile, so every metric in luxd registered into a
black hole while --api-metrics-enabled=true still advertised metrics as
on. Whether metrics are served is the runtime flag's call; a build tag
must not silently overrule it. `metrics` becomes a base tag on every
profile.
Verified: the three new handler tests fail against the jsonv2 encoder
(checks decode empty, error field nil — the exact production symptom)
and pass after. `go tool nm` shows (*registry).Gather absent from a
default build and present once the tag is set.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
83 lines
2.8 KiB
Go
83 lines
2.8 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package health
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/gorilla/rpc/v2"
|
|
|
|
apihealth "github.com/luxfi/api/health"
|
|
"github.com/luxfi/log"
|
|
|
|
avajson "github.com/luxfi/node/utils/json"
|
|
)
|
|
|
|
// NewGetAndPostHandler returns a health handler that supports GET and jsonrpc
|
|
// POST requests.
|
|
func NewGetAndPostHandler(log log.Logger, reporter Reporter) (http.Handler, error) {
|
|
newServer := rpc.NewServer()
|
|
codec := avajson.NewCodec()
|
|
newServer.RegisterCodec(codec, "application/json")
|
|
newServer.RegisterCodec(codec, "application/json;charset=UTF-8")
|
|
|
|
getHandler := NewGetHandler(reporter.Health)
|
|
|
|
// If a GET request is sent, we respond with a 200 if the node is healthy or
|
|
// a 503 if the node isn't healthy.
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
newServer.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
getHandler.ServeHTTP(w, r)
|
|
})
|
|
|
|
err := newServer.RegisterService(NewService(log, reporter), "health")
|
|
return handler, err
|
|
}
|
|
|
|
// NewGetHandler return a health handler that supports GET requests reporting
|
|
// the result of the provided [reporter].
|
|
func NewGetHandler(reporter func(tags ...string) (map[string]apihealth.Result, bool)) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Make sure the content type is set before writing the header.
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
tags := r.URL.Query()["tag"]
|
|
checks, healthy := reporter(tags...)
|
|
if !healthy {
|
|
// If a health check has failed, we should return a 503.
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
}
|
|
// One encoder for one wire type: apihealth.APIReply is defined with
|
|
// encoding/json tags and carries a time.Duration per check, so it is
|
|
// encoding/json that defines its representation. The POST (jsonrpc)
|
|
// path already encodes it through that codec; encoding it here the
|
|
// same way is what makes GET and POST agree. jsonv2 cannot encode
|
|
// this type at all — time.Duration has no default representation
|
|
// there — so every GET reply used to degrade to the error fallback
|
|
// below. encoding/json also replaces invalid UTF-8 (check Details
|
|
// may embed raw chain-ID bytes) with U+FFFD rather than failing.
|
|
//
|
|
// Buffer first: a streaming encoder that errors part-way would leave
|
|
// the client a torn body whose Content-Length matches the
|
|
// truncation. Buffering makes the reply atomic.
|
|
var buf bytes.Buffer
|
|
err := json.NewEncoder(&buf).Encode(apihealth.APIReply{
|
|
Checks: checks,
|
|
Healthy: healthy,
|
|
})
|
|
if err != nil {
|
|
buf.Reset()
|
|
fmt.Fprintf(&buf, `{"healthy":%t,"error":"health reply encode failed"}`, healthy)
|
|
}
|
|
_, _ = w.Write(buf.Bytes())
|
|
})
|
|
}
|