Package Status:

- FIX missing info statement to expose in result
- UPDATE documentation, test
This commit is contained in:
Nicolas JUHEL
2026-03-23 08:33:30 +01:00
parent 9aac46b522
commit 2df3f4ccf8
11 changed files with 318 additions and 41 deletions
+4
View File
@@ -20,6 +20,10 @@
coverage*.out
coverage*.txt
coverage*.log
res_*.log
res_*.svg
res_*.out
res_*.txt
# Dependency directories (remove the comment below to include it)
vendor/
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Nicolas JUHEL
Copyright (c) 2020-2026 Nicolas JUHEL
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+7 -5
View File
@@ -51,8 +51,9 @@ func (c *Mode) unmarshall(val []byte) error {
// (e.g., "Must", "Should").
//
// Example:
// data, _ := json.Marshal(control.Must)
// // data is []byte(`"Must"`)
//
// data, _ := json.Marshal(control.Must)
// // data is []byte(`"Must"`)
func (c Mode) MarshalJSON() ([]byte, error) {
return json.Marshal(c.String())
}
@@ -63,9 +64,10 @@ func (c Mode) MarshalJSON() ([]byte, error) {
// it defaults to `Ignore`.
//
// Example:
// var m control.Mode
// json.Unmarshal([]byte(`"must"`), &m)
// // m is control.Must
//
// var m control.Mode
// json.Unmarshal([]byte(`"must"`), &m)
// // m is control.Must
func (c *Mode) UnmarshalJSON(bytes []byte) error {
var s string
if err := json.Unmarshal(bytes, &s); err != nil {
+2 -2
View File
@@ -353,9 +353,9 @@ encountered, following this hierarchy: `KO` > `WARN` > `OK`.
2. **Iteration**: The system iterates through all registered monitors.
3. **Mode Application**: For each component, its control mode is determined.
- If a component is part of a `Must` group and its status is `KO`, the global status
immediately becomes `KO`.
immediately becomes `KO`.
- If a component is part of a `Must` or `Should` group and its status is `WARN`, the
global status is elevated to `WARN` (if it was previously `OK`).
global status is elevated to `WARN` (if it was previously `OK`).
- `AnyOf` and `Quorum` groups are evaluated based on their specific rules.
4. **Finalization**: The final aggregated status is cached and returned.
+3
View File
@@ -41,6 +41,7 @@ import (
type encMod struct {
Mode stsctr.Mode `json:"mode"`
Name string `json:"name"`
Info map[string]interface{} `json:"info"`
Sts monsts.Status `json:"status"`
Cpt map[string]montps.MonitorStatus `json:"components"`
}
@@ -112,6 +113,7 @@ func (o modControl) getEncControl() []encMod {
var ctr = encMod{
Mode: m.GetMode(),
Name: m.GetName(),
Info: m.GetInfo(),
Cpt: make(map[string]montps.MonitorStatus),
}
@@ -136,6 +138,7 @@ func (o modControl) getEncControl() []encMod {
var ctr = encMod{
Mode: stsctr.Ignore,
Name: "not-defined",
Info: make(map[string]interface{}),
Sts: monsts.OK,
Cpt: make(map[string]montps.MonitorStatus),
}
+4
View File
@@ -83,6 +83,10 @@ func (o *sts) SetVersion(v libver.Version) {
o.m.Lock()
defer o.m.Unlock()
if v == nil {
return
}
o.fn = v.GetPackage
o.fr = v.GetRelease
o.fh = v.GetBuild
+4 -3
View File
@@ -209,8 +209,8 @@ type Status interface {
//
// The returned instance is thread-safe but requires further configuration before it
// can be used effectively. At a minimum, you must:
// 1. Call `SetInfo` or `SetVersion` to provide application identity.
// 2. Call `RegisterPool` to link a monitor pool for health checks.
// 1. Call `SetInfo` or `SetVersion` to provide application identity.
// 2. Call `RegisterPool` to link a monitor pool for health checks.
//
// You can also optionally call `SetConfig` to define custom health policies and
// `SetErrorReturn` to customize error formatting.
@@ -219,7 +219,8 @@ type Status interface {
// - ctx: The root `context.Context` for the application.
//
// Returns:
// A new `Status` instance.
//
// A new `Status` instance.
func New(ctx context.Context) Status {
s := &sts{
m: sync.RWMutex{},
+26 -26
View File
@@ -53,33 +53,33 @@
// The following diagram illustrates how the `Mandatory` package interacts with the
// broader status monitoring workflow:
//
// +------------------+ +--------------------+
// | Configuration | | Status Monitor |
// | (Static/Dynamic) | | (Poller) |
// +--------+---------+ +---------+----------+
// | |
// v v
// +--------+-----------------------------+----------+
// | Mandatory |
// | |
// | +------------+ +-------------------+ |
// | | Key Set | <------- | Is Key Mandatory? | |
// | | {A, B, C} | | (KeyHas) | |
// | +------------+ +-------------------+ |
// | ^ |
// | | Add/Del |
// | v |
// | +------------+ +-------------------+ |
// | | Validation | -------> | Get Strategy | |
// | | Mode | | (GetMode) | |
// | +------------+ +-------------------+ |
// | |
// +-------------------------------------------------+
// +------------------+ +--------------------+
// | Configuration | | Status Monitor |
// | (Static/Dynamic) | | (Poller) |
// +--------+---------+ +---------+----------+
// | |
// v v
// +--------+-----------------------------+----------+
// | Mandatory |
// | |
// | +------------+ +-------------------+ |
// | | Key Set | <------- | Is Key Mandatory? | |
// | | {A, B, C} | | (KeyHas) | |
// | +------------+ +-------------------+ |
// | ^ |
// | | Add/Del |
// | v |
// | +------------+ +-------------------+ |
// | | Validation | -------> | Get Strategy | |
// | | Mode | | (GetMode) | |
// | +------------+ +-------------------+ |
// | |
// +-------------------------------------------------+
//
// 1. **Configuration Phase**: The application defines groups (e.g., "Critical Services")
// and populates them with component keys using `KeyAdd`. The validation strategy
// is set using `SetMode` (e.g., `control.Must`), and descriptive metadata is
// added via `SetInfo` or `AddInfo`.
// 1. **Configuration Phase**: The application defines groups (e.g., "Critical Services")
// and populates them with component keys using `KeyAdd`. The validation strategy
// is set using `SetMode` (e.g., `control.Must`), and descriptive metadata is
// added via `SetInfo` or `AddInfo`.
//
// 2. **Monitoring Phase**: When the status system evaluates the overall health:
// - It iterates over registered components.
+8 -4
View File
@@ -110,7 +110,8 @@ func (o *sts) IsCacheStrictlyHealthy() bool {
// - name: An optional list of component names to check. If empty, checks all components.
//
// Returns:
// `true` if the aggregated status is `OK` or `WARN`, `false` otherwise.
//
// `true` if the aggregated status is `OK` or `WARN`, `false` otherwise.
func (o *sts) IsHealthy(name ...string) bool {
s, _ := o.getStatus(name...)
return s >= monsts.Warn
@@ -126,7 +127,8 @@ func (o *sts) IsHealthy(name ...string) bool {
// - name: An optional list of component names to check. If empty, checks all components.
//
// Returns:
// `true` only if the aggregated status is `OK`, `false` otherwise.
//
// `true` only if the aggregated status is `OK`, `false` otherwise.
func (o *sts) IsStrictlyHealthy(name ...string) bool {
s, _ := o.getStrictStatus(name...)
return s == monsts.OK
@@ -148,7 +150,8 @@ func (o *sts) IsStrictlyHealthy(name ...string) bool {
// If empty, all configured components are considered.
//
// Returns:
// The computed `monsts.Status` and a message from the component that caused the degradation.
//
// The computed `monsts.Status` and a message from the component that caused the degradation.
func (o *sts) getStatus(keys ...string) (monsts.Status, string) {
stt := monsts.OK
msg := ""
@@ -262,7 +265,8 @@ func (o *sts) getStatus(keys ...string) (monsts.Status, string) {
// If empty, all configured components are considered.
//
// Returns:
// The "worst" `monsts.Status` found among the checked components and its corresponding message.
//
// The "worst" `monsts.Status` found among the checked components and its corresponding message.
func (o *sts) getStrictStatus(keys ...string) (monsts.Status, string) {
stt := monsts.OK
msg := ""
+80
View File
@@ -636,6 +636,69 @@ var _ = Describe("Status/Route", func() {
Expect(ok).To(BeTrue())
Expect(lst).To(HaveKey("test-service"))
})
It("should include mandatory group info in map mode", func() {
status := libsts.New(globalCtx)
status.SetInfo("route-test", "v1.0.0", "abc123")
pool := newPool()
status.RegisterPool(func() montps.Pool { return pool })
router := ginsdk.New()
router.GET("/status", func(c *ginsdk.Context) {
status.MiddleWare(c)
})
// Configure mandatory group with Info
cfg := libsts.Config{
Component: []libsts.Mandatory{
{
Name: "info-group",
Mode: stsctr.Must,
Keys: []string{"test-service"},
Info: map[string]interface{}{
"description": "Critical Service Group",
"link": "https://example.com/docs",
},
},
},
}
status.SetConfig(cfg)
m := newHealthyMonitor("test-service")
err := pool.MonitorAdd(m)
Expect(err).ToNot(HaveOccurred())
time.Sleep(testMonitorStabilizeDelay)
req := httptest.NewRequest("GET", "/status?map=true", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
Expect(w.Code).To(Equal(http.StatusOK))
var result map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &result)
Expect(err).ToNot(HaveOccurred())
val, ok := result["component"]
Expect(ok).To(BeTrue())
itm, ok := val.([]interface{})
Expect(ok).To(BeTrue())
Expect(itm).To(HaveLen(1))
sub, ok := itm[0].(map[string]interface{})
Expect(ok).To(BeTrue())
// Verify the info field exists and contains correct data
infoField, ok := sub["info"]
Expect(ok).To(BeTrue(), "info field should exist in map mode output")
infoMap, ok := infoField.(map[string]interface{})
Expect(ok).To(BeTrue(), "info field should be a map")
Expect(infoMap).To(HaveKeyWithValue("description", "Critical Service Group"))
Expect(infoMap).To(HaveKeyWithValue("link", "https://example.com/docs"))
})
})
Context("with filter query parameter", func() {
@@ -663,11 +726,19 @@ var _ = Describe("Status/Route", func() {
Name: "group-a",
Mode: stsctr.Must,
Keys: []string{"service-a"},
Info: map[string]interface{}{
"description": "Critical Service Group A",
"link": "https://example.com/docs/a",
},
},
{
Name: "group-b",
Mode: stsctr.Should,
Keys: []string{"service-b"},
Info: map[string]interface{}{
"description": "Critical Service Group B",
"link": "https://example.com/docs/b",
},
},
},
}
@@ -733,6 +804,15 @@ var _ = Describe("Status/Route", func() {
Expect(ok).To(BeTrue())
Expect(group).To(HaveKeyWithValue("name", "group-a"))
// Verify the info field exists and contains correct data
infoField, ok := group["info"]
Expect(ok).To(BeTrue(), "info field should exist in map mode output")
infoMap, ok := infoField.(map[string]interface{})
Expect(ok).To(BeTrue(), "info field should be a map")
Expect(infoMap).To(HaveKeyWithValue("description", "Critical Service Group A"))
Expect(infoMap).To(HaveKeyWithValue("link", "https://example.com/docs/a"))
components, ok := group["components"].(map[string]interface{})
Expect(ok).To(BeTrue())
Expect(components).To(HaveLen(1))
+179
View File
@@ -0,0 +1,179 @@
#!/bin/bash
#
# MIT License
#
# Copyright (c) 2026 Nicolas JUHEL
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#set -e
# Default package to current directory if not provided
PKG="${1:-.}"
TIMEOUT="2m"
# Determine Output Directory
# Strip common suffixes to find the directory part
CLEAN_PKG="$PKG"
CLEAN_PKG="${CLEAN_PKG%...}"
CLEAN_PKG="${CLEAN_PKG%/}"
if [ -d "$CLEAN_PKG" ]; then
LOG_DIR="$CLEAN_PKG"
else
# If the package path isn't a directory (e.g. a go module path), default to current
LOG_DIR="."
fi
echo "Running tests for package: $PKG"
echo "Logs and metrics will be stored in: $LOG_DIR"
# Define output files
F_COV="$LOG_DIR/res_coverage.log"
F_COV_RACE="$LOG_DIR/res_coverage_race.log"
F_LOG_TEST="$LOG_DIR/res_test.log"
F_LOG_RACE="$LOG_DIR/res_test_race.log"
F_LOG_BENCH="$LOG_DIR/res_bench.log"
F_CPULST="$LOG_DIR/res_cpu-list.log"
F_CPUSVG="$LOG_DIR/res_cpu.svg"
F_MEMLST="$LOG_DIR/res_mem-list.log"
F_MEMSVG="$LOG_DIR/res_mem.svg"
F_MEMTOP="$LOG_DIR/res_mem-top.log"
F_REPORT="$LOG_DIR/res_report.log"
F_LSHW="$LOG_DIR/res_lshw.log"
F_LOG_SEC="$LOG_DIR/res_gosec.log"
F_LOG_LINT="$LOG_DIR/res_golint.log"
# Clean up previous artifacts
rm -f "$F_COV" "$F_COV.out" "$F_COV_RACE" "$F_COV_RACE.out" "$F_LOG_TEST" "$F_LOG_RACE"
rm -f "$F_LOG_BENCH" "$F_CPULST" "$F_CPULST.out" "$F_CPUSVG" "$F_MEMLST" "$F_MEMLST.out" "$F_MEMSVG" "$F_MEMTOP"
rm -f "$F_REPORT" "$F_LSHW" "$F_LOG_SEC" "$F_LOG_LINT"
# Testing sudo
sudo echo "ok"
# 1. Print Material used for test
# Capture both script messages and command output to file
{
echo "----------------------------------------------------------------------"
echo "Listing materials..."
echo "----------------------------------------------------------------------"
sudo lshw
} > "$F_LSHW" 2>&1
echo "Step 1/7: Listing materials. Logs: $F_LSHW"
# 2. Calling Reports script
# Capture both script messages and command output to file
$(dirname "${0}")/coverage-report.sh -o "$(basename "$F_REPORT")" "${CLEAN_PKG#./}"
echo "Step 2/7: Report called. Logs: $F_REPORT"
# 3. Normal Test Mode with Coverage
# Capture both script messages and command output to file
{
echo "----------------------------------------------------------------------"
echo "Running Tests (Normal Mode) with Coverage..."
echo "Package: $PKG"
echo "Timeout: $TIMEOUT"
echo "Mode: atomic"
echo "----------------------------------------------------------------------"
go test -v -timeout "$TIMEOUT" -covermode=atomic -coverprofile="$F_COV.out" "$PKG"
go tool cover -func="$F_COV.out" -o="$F_COV"
rm -f "$F_COV.out"
} > "$F_LOG_TEST" 2>&1
echo "Step 3/7: Normal Tests completed. Logs: $F_LOG_TEST"
# 4. Benchmarks (Normal Mode)
{
echo "----------------------------------------------------------------------"
echo "Running Benchmarks..."
echo "Package: $PKG"
echo "Flags: -bench=. -benchmem"
echo "----------------------------------------------------------------------"
go test -v -run=^$ -bench=. -benchmem -cpuprofile="$F_CPULST.out" -memprofile="$F_MEMLST.out" "$PKG"
go tool pprof -svg "$F_CPULST.out" > "$F_CPUSVG"
go tool pprof -list . "$F_CPULST.out" > "$F_CPULST"
go tool pprof -svg "$F_MEMLST.out" > "$F_MEMSVG"
go tool pprof -list . "$F_MEMLST.out" > "$F_MEMLST"
go tool pprof -top . "$F_MEMLST.out" > "$F_MEMTOP"
rm -f "$F_CPULST.out" "$F_MEMLST.out"
} > "$F_LOG_BENCH" 2>&1
echo "Step 4/7: Benchmarks completed. Logs: $F_LOG_BENCH"
# 5. Race Test Mode with Coverage
{
echo "----------------------------------------------------------------------"
echo "Running Tests (Race Mode) with Coverage..."
echo "Package: $PKG"
echo "Timeout: $TIMEOUT"
echo "Mode: atomic + race"
echo "----------------------------------------------------------------------"
export CGO_ENABLED=1
go test -v -race -timeout "$TIMEOUT" -covermode=atomic -coverprofile="$F_COV_RACE.out" "$PKG"
export CGO_ENABLED=0
go tool cover -func="$F_COV_RACE.out" -o="$F_COV_RACE"
rm -f "$F_COV_RACE.out"
} > "$F_LOG_RACE" 2>&1
echo "Step 5/7: Race Tests completed. Logs: $F_LOG_RACE"
# 6. Checking security static code
{
echo "----------------------------------------------------------------------"
echo "Checking static security ..."
echo "Package: $PKG"
echo "----------------------------------------------------------------------"
gosec -sort "$PKG"
} > "$F_LOG_SEC" 2>&1
echo "Step 6/7: Checking static security completed. Logs: $F_LOG_SEC"
# 7. Verify Golint
{
echo "----------------------------------------------------------------------"
echo "Checking / Updating format & imports..."
echo "Package: $PKG"
echo "----------------------------------------------------------------------"
for ITM in $(find "$LOG_DIR" -type f -name '*.go' | grep -v '/vendor/')
do
gofmt -w "$ITM"
go fmt "$ITM"
goimports -w "$ITM"
done
echo "----------------------------------------------------------------------"
echo "Checking linters..."
echo "Package: $PKG"
echo "----------------------------------------------------------------------"
golangci-lint --config .golangci.yml run "$PKG"
} > "$F_LOG_LINT" 2>&1
echo "Step 7/7: Checking format, imports & linter completed. Logs: $F_LOG_LINT"
echo "----------------------------------------------------------------------"
echo "All operations completed successfully."
echo "Artifacts in $LOG_DIR:"
echo " - Logs: test.log, test_race.log, bench.log"
echo " - Coverage: coverage.out, coverage_race.out"
echo " - Profiles: cpu.out, mem.out"
echo " - Quality: gosec.log golint.log"
echo " - Reports: lshw.log, report.log"
echo "----------------------------------------------------------------------"