diff --git a/.gitignore b/.gitignore index 3200c05..ad5e9d6 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/LICENSE b/LICENSE index 3c4981d..cec79a0 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/status/control/encode.go b/status/control/encode.go index f03366c..23e57cf 100644 --- a/status/control/encode.go +++ b/status/control/encode.go @@ -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 { diff --git a/status/doc.go b/status/doc.go index c24c879..6245d8b 100644 --- a/status/doc.go +++ b/status/doc.go @@ -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. diff --git a/status/encodeMap.go b/status/encodeMap.go index bc0de58..e3abbed 100644 --- a/status/encodeMap.go +++ b/status/encodeMap.go @@ -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), } diff --git a/status/info.go b/status/info.go index 759225b..ab6ab4e 100644 --- a/status/info.go +++ b/status/info.go @@ -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 diff --git a/status/interface.go b/status/interface.go index ba93227..ce02b23 100644 --- a/status/interface.go +++ b/status/interface.go @@ -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{}, diff --git a/status/mandatory/doc.go b/status/mandatory/doc.go index 9bf86a6..28746fc 100644 --- a/status/mandatory/doc.go +++ b/status/mandatory/doc.go @@ -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. diff --git a/status/model.go b/status/model.go index 48dcd42..7b4804c 100644 --- a/status/model.go +++ b/status/model.go @@ -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 := "" diff --git a/status/route_test.go b/status/route_test.go index 1b93996..bc0d479 100644 --- a/status/route_test.go +++ b/status/route_test.go @@ -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)) diff --git a/test_bench_log.sh b/test_bench_log.sh new file mode 100755 index 0000000..123be12 --- /dev/null +++ b/test_bench_log.sh @@ -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 "----------------------------------------------------------------------"