mirror of
https://github.com/nabbar/golib.git
synced 2026-04-22 23:17:12 +08:00
7b59ce5f50
- ADD features to allow filtering component or mandatory list in api request - ADD name properties to mandatory list allow filter on it - ADD default naming capabilit for mandatory if not set - ADD metadata information for mandatory, component and main status - UPDATE testing and documentation - FIX minor bug to prevent panic Other: - BUMP dependencies
481 lines
16 KiB
Go
481 lines
16 KiB
Go
/*
|
|
* MIT License
|
|
*
|
|
* Copyright (c) 2022 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.
|
|
*
|
|
*/
|
|
|
|
package status
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"path"
|
|
|
|
libval "github.com/go-playground/validator/v10"
|
|
monsts "github.com/nabbar/golib/monitor/status"
|
|
stsctr "github.com/nabbar/golib/status/control"
|
|
stslmd "github.com/nabbar/golib/status/listmandatory"
|
|
stsmdt "github.com/nabbar/golib/status/mandatory"
|
|
)
|
|
|
|
const (
|
|
// keyConfigReturnCode is the internal key used to store the HTTP return code
|
|
// mapping in the thread-safe context configuration.
|
|
keyConfigReturnCode = "cfgReturnCode"
|
|
|
|
// keyConfigInfo is the internal key used to store the global service information
|
|
// (description, links) in the thread-safe context configuration.
|
|
keyConfigInfo = "cfgInfo"
|
|
|
|
// keyConfigMandatory is the internal key used to store the list of mandatory
|
|
// component groups in the thread-safe context configuration.
|
|
keyConfigMandatory = "cfgMandatory"
|
|
)
|
|
|
|
// Mandatory defines a group of components that share a specific control mode and
|
|
// descriptive information. It allows grouping multiple components (e.g., "all databases")
|
|
// and defining how their collective health affects the overall application status.
|
|
//
|
|
// This structure is typically used when loading configuration from a file (JSON, YAML, etc.).
|
|
//
|
|
// See github.com/nabbar/golib/status/control for details on available control modes.
|
|
type Mandatory struct {
|
|
// Mode defines how this group of components affects the overall status.
|
|
// Possible values include:
|
|
// - Ignore: The components are monitored but do not affect global status.
|
|
// - Should: Failure causes a warning but not a critical failure.
|
|
// - Must: Failure causes a critical global failure.
|
|
// - AnyOf: At least one component in the group must be healthy.
|
|
// - Quorum: A majority of components in the group must be healthy.
|
|
Mode stsctr.Mode `mapstructure:"mode" json:"mode" yaml:"mode" toml:"mode" validate:"required"`
|
|
|
|
// Name is a unique identifier for the mandatory group. It is used for filtering
|
|
// and logging purposes.
|
|
Name string `mapstructure:"name" json:"name" yaml:"name" toml:"name"`
|
|
|
|
// Info contains descriptive metadata about the group, such as a human-readable
|
|
// description or links to runbooks and dashboards. This information is exposed
|
|
// in the status response to provide context to operators.
|
|
// Example: {"description": "Primary database cluster", "runbook": "https://.../fix-db"}
|
|
Info map[string]interface{} `mapstructure:"info" json:"info" yaml:"info" toml:"info"`
|
|
|
|
// Keys is a list of static monitor names belonging to this group.
|
|
// These names must match the names of the monitors registered in the monitor pool.
|
|
// This field is used when the component names are known at configuration time.
|
|
Keys []string `mapstructure:"keys" json:"keys" yaml:"keys" toml:"keys"`
|
|
|
|
// ConfigKeys is used to specify the keys of config components. This allows for
|
|
// dynamic resolution of monitor names. When `SetConfig` is called, the system
|
|
// will look up the component configuration using these keys (via the function
|
|
// registered with `RegisterGetConfigCpt`) and add the associated monitor names
|
|
// to this mandatory group.
|
|
ConfigKeys []string `mapstructure:"configKeys,omitempty" json:"configKeys,omitempty" yaml:"configKeys,omitempty" toml:"configKeys,omitempty"`
|
|
}
|
|
|
|
// ParseMandatory converts a `mandatory.Mandatory` interface (from the internal logic)
|
|
// to a `Mandatory` struct (for configuration/export).
|
|
//
|
|
// This is a utility function for converting between the runtime interface representation
|
|
// and the configuration struct representation.
|
|
//
|
|
// Parameters:
|
|
// - m: The `mandatory.Mandatory` interface to convert.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A `Mandatory` struct populated with the mode and keys from the interface.
|
|
// Returns an empty struct if the input is nil.
|
|
func ParseMandatory(m stsmdt.Mandatory) Mandatory {
|
|
if m == nil {
|
|
return Mandatory{}
|
|
}
|
|
|
|
return Mandatory{
|
|
Mode: m.GetMode(),
|
|
Name: m.GetName(),
|
|
Keys: m.KeyList(),
|
|
Info: m.GetInfo(),
|
|
ConfigKeys: nil, // ConfigKeys are resolved to Keys during runtime, so we don't export them back.
|
|
}
|
|
}
|
|
|
|
// ParseList converts a slice of `mandatory.Mandatory` interfaces to a slice of
|
|
// `Mandatory` structs.
|
|
//
|
|
// This is a utility function for bulk conversion, useful when exporting the current
|
|
// configuration state.
|
|
//
|
|
// Parameters:
|
|
// - m: A variadic list of `mandatory.Mandatory` interfaces.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A slice of `Mandatory` structs. Nil entries in the input are skipped.
|
|
func ParseList(m ...stsmdt.Mandatory) []Mandatory {
|
|
r := make([]Mandatory, 0, len(m))
|
|
for _, i := range m {
|
|
if i != nil {
|
|
r = append(r, ParseMandatory(i))
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Config defines the complete configuration for the status system.
|
|
// It controls how the application's health is computed and how it is reported
|
|
// via HTTP.
|
|
type Config struct {
|
|
// ReturnCode maps internal health status levels (OK, Warn, KO) to HTTP status codes.
|
|
//
|
|
// Default values if not set:
|
|
// - monsts.OK: 200 (http.StatusOK)
|
|
// - monsts.Warn: 207 (http.StatusMultiStatus)
|
|
// - monsts.KO: 500 (http.StatusInternalServerError)
|
|
ReturnCode map[monsts.Status]int `mapstructure:"return-code" json:"return-code" yaml:"return-code" toml:"return-code" validate:"required"`
|
|
|
|
// Info contains global descriptive metadata about the service, such as a
|
|
// human-readable description or links to documentation and dashboards.
|
|
// This information is exposed at the root of the status response.
|
|
// Example: {"description": "Main Order API", "documentation": "https://.../docs"}
|
|
Info map[string]interface{} `mapstructure:"info" json:"info" yaml:"info" toml:"info"`
|
|
|
|
// Component defines the list of mandatory component groups. Each group specifies
|
|
// a set of components and the control mode that applies to them.
|
|
Component []Mandatory `mapstructure:"component" json:"component" yaml:"component" toml:"component" validate:""`
|
|
}
|
|
|
|
// Validate checks if the configuration is valid using the `validator` package.
|
|
// It ensures that all required fields are present and meet the defined constraints.
|
|
//
|
|
// Returns:
|
|
//
|
|
// An error if validation fails, containing details about which fields failed
|
|
// and why. Returns nil if the configuration is valid.
|
|
func (o Config) Validate() error {
|
|
var e = ErrorValidatorError.Error(nil)
|
|
|
|
if err := libval.New().Struct(o); err != nil {
|
|
if er, ok := err.(*libval.InvalidValidationError); ok {
|
|
e.Add(er)
|
|
}
|
|
|
|
for _, er := range err.(libval.ValidationErrors) {
|
|
//nolint
|
|
e.Add(fmt.Errorf("config field '%s' is not validated by constraint '%s'", er.Namespace(), er.ActualTag()))
|
|
}
|
|
}
|
|
|
|
if !e.HasParent() {
|
|
e = nil
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
// RegisterGetConfigCpt registers a function that retrieves a component monitor
|
|
// configuration by its key.
|
|
//
|
|
// This function is the mechanism for dynamic component resolution. It is used
|
|
// by `SetConfig` to resolve `ConfigKeys` into actual monitor names.
|
|
//
|
|
// Parameters:
|
|
// - fct: The function to register. It should take a string key and return a
|
|
// `cfgtps.ComponentMonitor` interface.
|
|
func (o *sts) RegisterGetConfigCpt(fct FuncGetCfgCpt) {
|
|
o.m.Lock()
|
|
defer o.m.Unlock()
|
|
o.n = fct
|
|
}
|
|
|
|
// GetConfig retrieves the current configuration used by the status instance.
|
|
// It reconstructs the `Config` struct from the internal state stored in the
|
|
// context configuration.
|
|
//
|
|
// This is useful for inspecting the active configuration at runtime.
|
|
//
|
|
// Returns:
|
|
//
|
|
// The current `Config` object. If no configuration has been set, it returns
|
|
// a default configuration with standard HTTP codes and an empty component list.
|
|
func (o *sts) GetConfig() Config {
|
|
var (
|
|
cfg = Config{
|
|
ReturnCode: make(map[monsts.Status]int, 0),
|
|
Component: make([]Mandatory, 0),
|
|
}
|
|
)
|
|
|
|
// Set default return codes
|
|
cfg.ReturnCode[monsts.KO] = http.StatusInternalServerError
|
|
cfg.ReturnCode[monsts.Warn] = http.StatusMultiStatus
|
|
cfg.ReturnCode[monsts.OK] = http.StatusOK
|
|
|
|
// Try to load return codes from internal storage
|
|
if i, l := o.x.Load(keyConfigReturnCode); !l || i == nil {
|
|
// Not found or nil, keep defaults
|
|
} else if r, k := i.(map[monsts.Status]int); !k || len(r) < 1 {
|
|
// Invalid type or empty, keep defaults
|
|
} else {
|
|
cfg.ReturnCode = r
|
|
}
|
|
|
|
if i, l := o.x.Load(keyConfigInfo); !l || i == nil {
|
|
cfg.Info = make(map[string]interface{})
|
|
} else if v, k := i.(map[string]interface{}); !k || len(v) < 1 {
|
|
cfg.Info = make(map[string]interface{})
|
|
} else {
|
|
cfg.Info = v
|
|
}
|
|
|
|
// Try to load mandatory components from internal storage
|
|
if i, l := o.x.Load(keyConfigMandatory); !l || i == nil {
|
|
// Not found or nil, keep empty list
|
|
} else if r, k := i.(stslmd.ListMandatory); !k || r == nil || r.Len() < 1 {
|
|
// Invalid type or empty, keep empty list
|
|
} else {
|
|
// Reconstruct the slice of Mandatory structs from the internal list
|
|
r.Walk(func(_ string, m stsmdt.Mandatory) bool {
|
|
cfg.Component = append(cfg.Component, Mandatory{
|
|
Mode: m.GetMode(),
|
|
Name: m.GetName(),
|
|
Info: m.GetInfo(),
|
|
Keys: m.KeyList(),
|
|
ConfigKeys: nil,
|
|
})
|
|
return true
|
|
})
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
// SetConfig applies the given configuration to the status instance.
|
|
// It updates the HTTP return codes and the list of mandatory component groups.
|
|
//
|
|
// This method handles the logic for:
|
|
// 1. Setting default HTTP return codes if none are provided.
|
|
// 2. Creating a new internal list of mandatory groups.
|
|
// 3. Resolving dynamic `ConfigKeys` into monitor names using the registered
|
|
// resolver function (if available).
|
|
// 4. Storing the configuration in the thread-safe context storage.
|
|
//
|
|
// Parameters:
|
|
// - cfg: The configuration to apply.
|
|
func (o *sts) SetConfig(cfg Config) {
|
|
// Handle default return codes
|
|
if len(cfg.ReturnCode) < 1 {
|
|
var def = make(map[monsts.Status]int, 0)
|
|
def[monsts.KO] = http.StatusInternalServerError
|
|
def[monsts.Warn] = http.StatusMultiStatus
|
|
def[monsts.OK] = http.StatusOK
|
|
|
|
o.x.Store(keyConfigReturnCode, def)
|
|
} else {
|
|
o.x.Store(keyConfigReturnCode, cfg.ReturnCode)
|
|
}
|
|
|
|
if len(cfg.Info) > 0 {
|
|
o.x.Store(keyConfigInfo, cfg.Info)
|
|
}
|
|
|
|
// Create a new list for mandatory groups
|
|
var lst = stslmd.New()
|
|
|
|
if len(cfg.Component) > 0 {
|
|
for _, i := range cfg.Component {
|
|
var m = stsmdt.New()
|
|
m.SetMode(i.Mode)
|
|
m.SetName(stsmdt.GetNameOrDefault(i.Name))
|
|
m.SetInfo(i.Info)
|
|
|
|
// Add static keys
|
|
m.KeyAdd(i.Keys...)
|
|
|
|
// Resolve and add dynamic keys from ConfigKeys
|
|
if len(i.ConfigKeys) > 0 && o.n != nil {
|
|
for _, k := range i.ConfigKeys {
|
|
// Call the registered resolver function
|
|
if c := o.n(k); c != nil {
|
|
// Add the monitor names returned by the component
|
|
m.KeyAdd(c.GetMonitorNames()...)
|
|
}
|
|
}
|
|
}
|
|
lst.Add(m)
|
|
}
|
|
}
|
|
|
|
// Store the new list in the context configuration
|
|
o.x.Store(keyConfigMandatory, lst)
|
|
}
|
|
|
|
// cfgGetReturnCode retrieves the configured HTTP status code for a specific
|
|
// health status (OK, Warn, KO).
|
|
//
|
|
// It accesses the thread-safe configuration storage. If the configuration is
|
|
// missing or invalid, it defaults to `http.StatusInternalServerError` (500)
|
|
// as a safety measure.
|
|
//
|
|
// Parameters:
|
|
// - s: The health status to look up.
|
|
//
|
|
// Returns:
|
|
//
|
|
// The corresponding HTTP status code.
|
|
func (o *sts) cfgGetReturnCode(s monsts.Status) int {
|
|
if i, l := o.x.Load(keyConfigReturnCode); !l {
|
|
return http.StatusInternalServerError
|
|
} else if v, k := i.(map[monsts.Status]int); !k {
|
|
return http.StatusInternalServerError
|
|
} else if r, f := v[s]; !f {
|
|
return http.StatusInternalServerError
|
|
} else {
|
|
return r
|
|
}
|
|
}
|
|
|
|
// cfgGetInfo retrieves the global service information (description, links) from
|
|
// the thread-safe configuration storage.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A map containing the global service metadata. Returns an empty map if not configured.
|
|
func (o *sts) cfgGetInfo() map[string]interface{} {
|
|
if i, l := o.x.Load(keyConfigInfo); !l {
|
|
return map[string]interface{}{}
|
|
} else if v, k := i.(map[string]interface{}); !k {
|
|
return map[string]interface{}{}
|
|
} else {
|
|
return v
|
|
}
|
|
}
|
|
|
|
// cfgGetMandatory retrieves the internal list of mandatory component groups.
|
|
//
|
|
// It accesses the thread-safe configuration storage.
|
|
//
|
|
// Returns:
|
|
//
|
|
// The `stslmd.ListMandatory` interface containing the groups, or nil if
|
|
// not configured.
|
|
func (o *sts) cfgGetMandatory() stslmd.ListMandatory {
|
|
if i, l := o.x.Load(keyConfigMandatory); !l {
|
|
return nil
|
|
} else if v, k := i.(stslmd.ListMandatory); !k {
|
|
return nil
|
|
} else {
|
|
return v
|
|
}
|
|
}
|
|
|
|
// cfgFilterMandatory filters the list of mandatory groups based on a list of patterns.
|
|
// It uses `path.Match` to support shell-style wildcards against the *name* of each
|
|
// mandatory group.
|
|
//
|
|
// Parameters:
|
|
// - filter: A slice of patterns to match against mandatory group names.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A new `stslmd.ListMandatory` containing only the groups that match at least
|
|
// one of the filter patterns. Returns nil if no groups match or if the initial
|
|
// list is empty.
|
|
func (o *sts) cfgFilterMandatory(filter []string) stslmd.ListMandatory {
|
|
var (
|
|
lst = o.cfgGetMandatory()
|
|
tmp = make([]stsmdt.Mandatory, 0)
|
|
)
|
|
|
|
if lst == nil || lst.Len() < 1 {
|
|
return nil
|
|
}
|
|
|
|
if len(filter) < 1 {
|
|
return nil
|
|
}
|
|
|
|
lst.Walk(func(k string, m stsmdt.Mandatory) bool {
|
|
for _, f := range filter {
|
|
if v, e := path.Match(f, k); e == nil && v {
|
|
tmp = append(tmp, m)
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if len(tmp) < 1 {
|
|
return nil
|
|
}
|
|
|
|
return stslmd.New(tmp...)
|
|
}
|
|
|
|
// cfgGetMode retrieves the control mode for a specific component key.
|
|
//
|
|
// It delegates the lookup to the internal mandatory list. If the component
|
|
// is not found in any mandatory group, it returns `stsctr.Ignore`.
|
|
//
|
|
// Parameters:
|
|
// - key: The component name to look up.
|
|
//
|
|
// Returns:
|
|
//
|
|
// The `stsctr.Mode` associated with the component.
|
|
func (o *sts) cfgGetMode(key string) stsctr.Mode {
|
|
if l := o.cfgGetMandatory(); l == nil {
|
|
return stsctr.Ignore
|
|
} else {
|
|
return l.GetMode(key)
|
|
}
|
|
}
|
|
|
|
// cfgGetOne retrieves all component names that belong to the same mandatory
|
|
// group as the specified key.
|
|
//
|
|
// This is used for evaluating group-based control modes like `AnyOf` and `Quorum`,
|
|
// where the status of one component depends on the status of its peers in the group.
|
|
//
|
|
// Parameters:
|
|
// - key: The component name to find the group for.
|
|
//
|
|
// Returns:
|
|
//
|
|
// A slice of strings containing all keys in the matching group. Returns an
|
|
// empty slice if the key is not found or no groups are configured.
|
|
func (o *sts) cfgGetOne(key string) []string {
|
|
if l := o.cfgGetMandatory(); l == nil {
|
|
return make([]string, 0)
|
|
} else {
|
|
var r []string
|
|
// Walk through the list to find the group containing the key
|
|
l.Walk(func(_ string, m stsmdt.Mandatory) bool {
|
|
if m.KeyHas(key) {
|
|
r = m.KeyList()
|
|
return false // Stop searching once found
|
|
}
|
|
|
|
return true
|
|
})
|
|
return r
|
|
}
|
|
}
|