Documentation
¶
Overview ¶
Package info provides a robust and thread-safe metadata management system for monitored components. It acts as a dynamic repository for identifying information (Name) and descriptive key-value pairs (Data) about a service, application, or internal component.
Core Philosophy ¶
The 'info' package is designed to bridge the gap between static configuration and dynamic runtime state. It allows a component to have a permanent identity (Default Name) while also supporting manual overrides and real-time data injection through provider functions.
Architecture & Internal Logic ¶
The internal state is managed by an 'inf' structure which utilizes a high-performance atomic map (libatm.Map) to ensure non-blocking, concurrent access from multiple health check routines or API endpoints.
## Naming Resolution Hierarchy
When the Name() method is invoked, the package follows a strict priority-based resolution logic:
- Dynamic Provider: If a function is registered via RegisterName(), it is executed. If it returns a non-empty string, this result is used.
- Manual Override: If no dynamic result is available, the package checks for a name set manually via SetName().
- Default Fallback: If neither of the above exists, the default name provided at initialization (via New()) is returned.
Naming Dataflow Diagram:
[ Call Name() ]
|
+--> [ Check Dynamic Provider (RegisterName) ] --(exists & non-empty)--> [ RETURN RESULT ]
|
+--> [ Check Manual Override (SetName) ] --------(exists)--------------> [ RETURN RESULT ]
|
+--> [ Default Name (New) ] -------------------------------------------> [ RETURN RESULT ]
## Metadata (Data) Resolution Logic
The Data() method aggregates information from two distinct layers:
- Static Store: Key-value pairs explicitly added via AddData() or SetData().
- Dynamic Provider: A map returned by the function registered via RegisterData().
The resolution logic merges these layers into a single map. In the event of a key collision, the data from the Dynamic Provider takes precedence, ensuring that real-time runtime values always override stale static configuration.
Metadata Dataflow Diagram:
[ Call Data() ]
|
+--[ Retrieve all static keys from Atomic Map ]
|
+--[ Execute Dynamic Provider (RegisterData) ]
|
+--[ Merge Logic: Dynamic values OVERWRITE static values ]
|
+--> [ RETURN MERGED MAP ]
Key Features ¶
- Thread-Safe: Atomic operations ensure consistency without the overhead of heavy mutexes.
- Zero-Caching Policy: Provider functions are executed on every call, ensuring that data like CPU usage, memory stats, or current version is always fresh.
- LIFO-like Cleanliness: SetData() replaces only user-defined metadata, preserving internal naming logic and registered functions.
- Standard Interfaces: Implements encoding.TextMarshaler and json.Marshaler for seamless integration with logging frameworks and REST APIs.
Usage Examples ¶
## Initializing and Adding Static Data
inf, _ := info.New("my-database-service")
inf.AddData("environment", "production")
inf.AddData("cluster", "eu-west-1")
## Registering Dynamic Metadata
This is useful for exposing real-time metrics alongside the component's identity.
inf.RegisterData(func() (map[string]interface{}, error) {
return map[string]interface{}{
"goroutines": runtime.NumGoroutine(),
"uptime": time.Since(startTime).String(),
}, nil
})
## Manual Name Override
inf.SetName("temporary-maintenance-mode")
// Name() now returns "temporary-maintenance-mode" instead of "my-database-service".
Thread Safety & Performance ¶
The internal implementation uses a lock-free approach where possible, relying on atomic stores and loads. This ensures that even under heavy load (e.g., thousands of status requests per second), the impact on the monitored component's performance is negligible.
Integration with Monitor ¶
This package is a core dependency of the 'monitor' package. In a typical monitoring setup, the 'Info' instance is passed to the monitor, which then uses it to enrich health check reports and Prometheus metrics with the component's identity and metadata.
Example (Caching) ¶
Example_caching demonstrates the caching behavior.
package main
import (
"fmt"
"github.com/nabbar/golib/monitor/info"
)
func main() {
i, _ := info.New("service")
callCount := 0
i.RegisterName(func() (string, error) {
callCount++
return fmt.Sprintf("name-%d", callCount), nil
})
// First call executes the function
name1 := i.Name()
fmt.Printf("First call: %s (callCount: %d)\n", name1, callCount)
// Second call uses cached value
name2 := i.Name()
fmt.Printf("Second call: %s (callCount: %d)\n", name2, callCount)
}
Output: First call: name-1 (callCount: 1) Second call: name-2 (callCount: 2)
Example (ErrorHandling) ¶
Example_errorHandling demonstrates error handling.
package main
import (
"fmt"
"github.com/nabbar/golib/monitor/info"
)
func main() {
i, _ := info.New("default-service")
i.RegisterName(func() (string, error) {
return "", fmt.Errorf("simulated error")
})
// Returns default name on error
name := i.Name()
fmt.Println(name)
}
Output: default-service
Example (MultipleInfo) ¶
Example_multipleInfo demonstrates handling multiple info registrations.
package main
import (
"fmt"
"github.com/nabbar/golib/monitor/info"
)
func main() {
i, _ := info.New("service")
// First registration
i.RegisterData(func() (map[string]interface{}, error) {
return map[string]interface{}{
"version": "1.0.0",
}, nil
})
fmt.Println("Version:", i.Data()["version"])
// Re-registration clears cache
i.RegisterData(func() (map[string]interface{}, error) {
return map[string]interface{}{
"version": "2.0.0",
}, nil
})
fmt.Println("Version:", i.Data()["version"])
}
Output: Version: 1.0.0 Version: 2.0.0
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Encode ¶
type Encode interface {
// String returns a single-line, human-readable string representation of the component's identification data.
// The standard format is: "ComponentName (key1: value1, key2: value2)".
// If no metadata is present, it returns only the name.
String() string
// Bytes returns the byte slice representation of the string generated by String().
// This is a convenience method for operations requiring a []byte instead of a string.
Bytes() []byte
}
Encode is an interface defining the contract for representing monitor information in a serializable format. It provides methods to generate both string and byte slice representations, typically used for logging, display, or as a component of larger status reports.
type Info ¶
Info is an interface that describes the behavior of a metadata container for monitored components. It extends the montps.InfoSet interface, which already incorporates read-only access (montps.Info) and write/update methods (montps.InfoSet). This structure is typically used to store identifying information (like a name) and dynamic status data (key-value pairs) about a monitored service. Implementations of this interface MUST ensure thread-safe operations as they are often accessed from concurrent health check routines and status reporting endpoints.
func New ¶
New initializes and returns a new instance of the Info interface, configured with a mandatory default name. This default name serves as a permanent fallback identifier for the monitored component.
Arguments:
- defaultName: A string used as the initial name and fallback for the component. It cannot be empty.
Returns:
- Info: A pointer to a thread-safe implementation of the Info interface.
- error: Returns an error if the defaultName provided is empty, indicating an invalid initialization attempt.
Example usage:
// Create a new Info instance for a database service.
inf, err := info.New("database-primary")
if err != nil {
log.Fatalf("Failed to initialize monitor info: %v", err)
}
// The default name is immediately available.
fmt.Println(inf.Name()) // Output: database-primary
Example ¶
ExampleNew demonstrates creating a new Info instance.
package main
import (
"fmt"
"log"
"github.com/nabbar/golib/monitor/info"
)
func main() {
i, err := info.New("my-service")
if err != nil {
log.Fatal(err)
}
fmt.Println(i.Name())
}
Output: my-service