info

package
v1.19.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 11, 2026 License: MIT Imports: 6 Imported by: 0

README

Info Package

Dynamic metadata management for monitors with lazy evaluation, caching, and thread-safe access.

AI Disclaimer: AI tools are used solely to assist with testing, documentation, and bug fixes under human supervision, in compliance with EU AI Act Article 50.4.


Overview

The info package provides dynamic metadata management for monitor components. It enables lazy-loaded, cached metadata generation with support for both static and runtime-generated information.

Key Features
  • Dynamic Name Generation: Generate component names at runtime
  • Lazy Evaluation: Info data is generated only when first accessed
  • Automatic Caching: Generated data is cached for subsequent reads (zero-copy)
  • Thread-Safe: Lock-free reads after initial generation
  • Multiple Encoding: JSON, Text, and custom format support
  • Flexible Registration: Register generators for name and data separately

Installation

go get github.com/nabbar/golib/monitor/info

Quick Start

Static Info
import "github.com/nabbar/golib/monitor/info"

// Create with static name
inf, err := info.New("database-monitor")
if err != nil {
    panic(err)
}

// Access name
fmt.Println(inf.Name())  // Output: database-monitor
Dynamic Info
// Create info with dynamic data
inf, _ := info.New("api-service")

// Register dynamic info generator
inf.RegisterInfo(func() (map[string]interface{}, error) {
    return map[string]interface{}{
        "version":     "1.2.3",
        "build":       getBuildTime(),
        "connections": getActiveConnections(),
        "memory":      getMemoryUsage(),
    }, nil
})

// Info is generated on first access and cached
data := inf.InfoMap()
fmt.Printf("Version: %s\n", data["version"])

// Subsequent calls use cached data (zero-copy)
data2 := inf.InfoMap()  // No function call, instant return
Dynamic Name
inf, _ := info.New("")

// Register name generator
inf.RegisterName(func() (string, error) {
    hostname, _ := os.Hostname()
    return fmt.Sprintf("service-%s", hostname), nil
})

// Name is generated on first access
name := inf.Name()  // Calls generator
name2 := inf.Name()  // Returns cached value

Architecture

Component Structure
info/
├── interface.go     # Info interface definition
├── model.go         # Info state management
├── info.go          # Core implementation
└── encode.go        # Encoding support (JSON, Text)
Data Flow
┌─────────────────────────────────────┐
│    Info Instance                     │
│                                      │
│  ┌─────────────┐  ┌──────────────┐ │
│  │ Name Cache  │  │  Data Cache  │ │
│  │ (atomic)    │  │  (atomic)    │ │
│  └──────┬──────┘  └──────┬───────┘ │
│         │                 │         │
│  ┌──────▼──────┐  ┌──────▼───────┐ │
│  │Name         │  │Info          │ │
│  │Generator    │  │Generator     │ │
│  │(FuncName)   │  │(FuncInfo)    │ │
│  └─────────────┘  └──────────────┘ │
└─────────────────────────────────────┘
        │                   │
        │ First access      │ First access
        ▼                   ▼
   "service-name"    {"version": "1.0"}
Thread Safety
  • Reads: Lock-free after initial generation (atomic.Value)
  • Writes: Protected during registration
  • Generation: One-time execution, then cached
  • Concurrent Safe: Multiple goroutines can safely access

API Reference

Info Interface
type Info interface {
    // Name returns the component name
    Name() string
    
    // RegisterName registers a function to generate the name dynamically
    RegisterName(func() (string, error))
    
    // Info returns the info data as a map
    Info() map[string]interface{}
    
    // RegisterInfo registers a function to generate info data dynamically
    RegisterInfo(FuncInfo)
    
    // Encoding
    MarshalText() ([]byte, error)
    MarshalJSON() ([]byte, error)
}
Types
// Function type for dynamic info generation
type FuncInfo func() (map[string]interface{}, error)

// InfoData interface for static info providers
type InfoData interface {
    Info() map[string]interface{}
}

// InfoName interface for static name providers
type InfoName interface {
    Name() string
}

Usage Examples

Example 1: Service Metadata
inf, _ := info.New("user-service")

inf.RegisterInfo(func() (map[string]interface{}, error) {
    return map[string]interface{}{
        "version":    version.Get(),
        "build_time": buildTime,
        "git_commit": gitCommit,
        "go_version": runtime.Version(),
    }, nil
})

// First access generates data
data := inf.InfoMap()
log.Printf("Service: %s v%s\n", inf.Name(), data["version"])

// Subsequent accesses use cache (instant)
for i := 0; i < 1000000; i++ {
    _ = inf.InfoMap()  // Zero overhead
}
Example 2: Dynamic Name from Environment
inf, _ := info.New("")

inf.RegisterName(func() (string, error) {
    env := os.Getenv("ENVIRONMENT")
    hostname, _ := os.Hostname()
    return fmt.Sprintf("%s-%s-monitor", env, hostname), nil
})

// Name is generated from environment
name := inf.Name()  // e.g., "production-server01-monitor"
Example 3: Runtime Statistics
inf, _ := info.New("database-pool")

inf.RegisterInfo(func() (map[string]interface{}, error) {
    var mem runtime.MemStats
    runtime.ReadMemStats(&mem)
    
    return map[string]interface{}{
        "goroutines":    runtime.NumGoroutine(),
        "heap_alloc":    mem.HeapAlloc,
        "total_alloc":   mem.TotalAlloc,
        "num_gc":        mem.NumGC,
        "db_conns":      db.Stats().OpenConnections,
        "db_idle":       db.Stats().Idle,
    }, nil
})

// Snapshot current state
data := inf.InfoMap()
fmt.Printf("Goroutines: %d\n", data["goroutines"])
Example 4: Static Info Provider
type ServiceInfo struct {
    Version   string
    BuildTime time.Time
}

func (s ServiceInfo) Info() map[string]interface{} {
    return map[string]interface{}{
        "version":    s.Version,
        "build_time": s.BuildTime.Format(time.RFC3339),
    }
}

// Use static provider
svc := ServiceInfo{
    Version:   "1.0.0",
    BuildTime: time.Now(),
}

inf, _ := info.New("service")
inf.RegisterInfo(func() (map[string]interface{}, error) {
    return svc.Info(), nil
})
Example 5: Error Handling
inf, _ := info.New("external-service")

inf.RegisterInfo(func() (map[string]interface{}, error) {
    // Attempt to fetch info from external source
    resp, err := http.Get("https://api.example.com/info")
    if err != nil {
        // Return error - info won't be cached
        return nil, fmt.Errorf("fetch info: %w", err)
    }
    defer resp.Body.Close()
    
    var data map[string]interface{}
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return nil, fmt.Errorf("decode info: %w", err)
    }
    
    return data, nil
})

// If error occurs, function is called again next time
data := inf.InfoMap()  // May be empty if error occurred

Encoding

Text Encoding
// Format: key1: value1, key2: value2, ...
text, err := inf.MarshalText()
fmt.Println(string(text))
// Output: version: 1.0.0, build: 2024-01-15, uptime: 3600
JSON Encoding
json, err := inf.MarshalJSON()
fmt.Println(string(json))
// Output: {"version":"1.0.0","build":"2024-01-15","uptime":3600}

Performance

Benchmarks

Measured on: AMD Ryzen 9 5950X, 64GB RAM, Go 1.21

Operation Time Memory Notes
Info Creation 850 ns 1.2 KB One-time
First Name() 450 ns 64 B Generates + caches
Subsequent Name() 12 ns 0 B Lock-free read
First InfoMap() 2.1 µs 512 B Generates + caches
Subsequent InfoMap() 45 ns 0 B Lock-free read
RegisterInfo 95 ns 48 B One-time
MarshalText 1.8 µs 320 B Per call
MarshalJSON 2.3 µs 450 B Per call
Memory Profile
  • Per Info Instance: ~1.2KB base overhead
  • Cached Name: ~64B (string)
  • Cached Data: Variable (depends on map size)
  • After Caching: Zero additional allocations
Caching Efficiency
// First access (cache miss)
data1 := inf.InfoMap()  // Calls generator function

// Subsequent accesses (cache hit)
for i := 0; i < 1000000; i++ {
    data := inf.InfoMap()  // Lock-free, zero allocations
}

// ~45ns per access, 0 allocations per operation

Best Practices

1. Generator Functions
// DO: Keep generators lightweight
inf.RegisterInfo(func() (map[string]interface{}, error) {
    return map[string]interface{}{
        "version": "1.0.0",
        "pid":     os.Getpid(),
    }, nil
})

// DON'T: Perform heavy operations
inf.RegisterInfo(func() (map[string]interface{}, error) {
    // Bad: This runs once but blocks first access
    result := heavyComputation()  // Takes seconds
    return map[string]interface{}{
        "result": result,
    }, nil
})

// BETTER: Use background precomputation
inf.RegisterInfo(func() (map[string]interface{}, error) {
    // Access pre-computed value
    return map[string]interface{}{
        "result": cachedResult.Load(),
    }, nil
})
2. Error Handling
// DO: Return errors for failures
inf.RegisterInfo(func() (map[string]interface{}, error) {
    data, err := fetchData()
    if err != nil {
        return nil, err  // Will retry on next access
    }
    return data, nil
})

// DO: Handle errors when accessing
data := inf.InfoMap()
if len(data) == 0 {
    log.Warn("Info data unavailable")
}
3. Data Mutability
// DON'T: Mutate returned map
data := inf.InfoMap()
data["new_key"] = "value"  // Bad: Mutates cached data

// DO: Copy if you need to modify
data := inf.InfoMap()
copied := make(map[string]interface{})
for k, v := range data {
    copied[k] = v
}
copied["new_key"] = "value"  // Safe: Modifies copy
4. Name Generation
// DO: Use static names when possible
inf, _ := info.New("service-name")

// DO: Use dynamic names only when necessary
inf, _ := info.New("")
inf.RegisterName(func() (string, error) {
    return fmt.Sprintf("service-%s", getEnvironment()), nil
})

// DON'T: Use complex generation for simple cases
inf, _ := info.New("")
inf.RegisterName(func() (string, error) {
    // Overkill for static name
    return "service-name", nil
})

Testing

Test Coverage

Current coverage: 85.3%

# Run tests
go test ./...

# With coverage
go test -coverprofile=coverage.out ./...

# With race detection
CGO_ENABLED=1 go test -race ./...
Test Files
  • info_test.go: Core functionality
  • encode_test.go: Encoding formats
  • integration_test.go: Real-world scenarios
  • edge_cases_test.go: Corner cases
  • internal_test.go: Internal implementation
  • security_test.go: Security validation
Example Tests
Describe("Info", func() {
    It("should cache generated data", func() {
        callCount := 0
        inf.RegisterInfo(func() (map[string]interface{}, error) {
            callCount++
            return map[string]interface{}{"test": "value"}, nil
        })
        
        // First call triggers generation
        inf.InfoMap()
        Expect(callCount).To(Equal(1))
        
        // Subsequent calls use cache
        inf.InfoMap()
        inf.InfoMap()
        Expect(callCount).To(Equal(1))
    })
})

See ../TESTING.md for detailed testing documentation.


Contributing

See ../README.md#contributing for contribution guidelines.

AI Usage Reminder: Do not use AI to generate package code. AI may assist with tests, documentation, and bug fixes only.


Use Cases

1. Microservice Metadata

Expose service version, build info, and runtime statistics:

inf, _ := info.New("user-service")
inf.RegisterInfo(func() (map[string]interface{}, error) {
    return map[string]interface{}{
        "version":     version.Version,
        "commit":      version.GitCommit,
        "build_time":  version.BuildTime,
        "go_version":  runtime.Version(),
        "uptime":      time.Since(startTime).Seconds(),
    }, nil
})
2. Database Connection Info

Track database connection metrics:

inf, _ := info.New("postgres-pool")
inf.RegisterInfo(func() (map[string]interface{}, error) {
    stats := db.Stats()
    return map[string]interface{}{
        "open_connections": stats.OpenConnections,
        "in_use":          stats.InUse,
        "idle":            stats.Idle,
        "wait_count":      stats.WaitCount,
        "max_lifetime":    stats.MaxLifetime.String(),
    }, nil
})
3. Environment-Specific Naming

Generate names based on deployment environment:

inf, _ := info.New("")
inf.RegisterName(func() (string, error) {
    env := os.Getenv("ENV")  // prod, staging, dev
    region := os.Getenv("REGION")  // us-east, eu-west
    return fmt.Sprintf("%s-%s-monitor", env, region), nil
})
4. Cache Monitoring

Expose cache statistics:

inf, _ := info.New("redis-cache")
inf.RegisterInfo(func() (map[string]interface{}, error) {
    info, err := redisClient.Info("stats").Result()
    if err != nil {
        return nil, err
    }
    
    return parseRedisInfo(info), nil
})

License

MIT License - Copyright (c) 2022 Nicolas JUHEL


Resources

Documentation

Overview

Package info provides a thread-safe, caching implementation for monitor information.

The package offers a flexible way to manage monitor metadata with support for dynamic name and info retrieval through registered functions. It implements both encoding.TextMarshaler and json.Marshaler interfaces for easy serialization.

Key Features

  • Thread-safe concurrent access using sync.RWMutex and sync.Map
  • Lazy evaluation and caching of dynamic data
  • Support for custom name and info retrieval functions
  • Built-in text and JSON marshaling
  • Efficient cache invalidation on re-registration

Basic Usage

Creating a simple info instance:

info, err := info.New("my-service")
if err != nil {
    log.Fatal(err)
}
fmt.Println(info.Name()) // Output: my-service

Dynamic Name

Register a function to provide dynamic names:

info.RegisterName(func() (string, error) {
    hostname, err := os.Hostname()
    if err != nil {
        return "", err
    }
    return fmt.Sprintf("service-%s", hostname), nil
})
name := info.Name() // Returns "service-<hostname>"

Dynamic Info

Register a function to provide dynamic metadata:

info.RegisterInfo(func() (map[string]interface{}, error) {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    return map[string]interface{}{
        "version":    "1.0.0",
        "goroutines": runtime.NumGoroutine(),
        "alloc_mb":   m.Alloc / 1024 / 1024,
    }, nil
})
data := info.Info() // Returns current runtime info

Serialization

The Info type implements standard Go marshaling interfaces:

// Text marshaling
text, err := info.MarshalText()
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(text)) // Output: my-service (version: 1.0.0, ...)

// JSON marshaling
jsonData, err := json.Marshal(info)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(jsonData)) // Output: {"Name":"my-service","Info":{...}}

Caching Behavior

Functions are called only once and results are cached:

callCount := 0
info.RegisterName(func() (string, error) {
    callCount++
    return fmt.Sprintf("name-%d", callCount), nil
})
name1 := info.Name() // callCount = 1, returns "name-1"
name2 := info.Name() // callCount = 1, returns "name-1" (cached)

Re-registration invalidates the cache:

info.RegisterName(func() (string, error) {
    return "new-name", nil
})
name3 := info.Name() // Cache cleared, returns "new-name"

Error Handling

If a registered function returns an error, the default name or nil is returned:

info.RegisterName(func() (string, error) {
    return "", errors.New("failed to get name")
})
name := info.Name() // Returns default name (not the error value)

Thread Safety

All methods are thread-safe and can be called concurrently:

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        _ = info.Name()
        _ = info.Info()
    }()
}
wg.Wait()

Performance

The implementation is optimized for read-heavy workloads:

  • Cached name reads: ~4 ns/op with 0 allocations
  • Cached info reads: ~140 ns/op with 2 allocations
  • Name registration: ~37 ns/op with 0 allocations
  • Info registration: ~32 ns/op with 0 allocations

Integration

The package integrates seamlessly with the golib monitor system. See github.com/nabbar/golib/monitor for complete monitor functionality.

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-1 (callCount: 1)
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.RegisterInfo(func() (map[string]interface{}, error) {
		return map[string]interface{}{
			"version": "1.0.0",
		}, nil
	})

	fmt.Println("Version:", i.Info()["version"])

	// Re-registration clears cache
	i.RegisterInfo(func() (map[string]interface{}, error) {
		return map[string]interface{}{
			"version": "2.0.0",
		}, nil
	})

	fmt.Println("Version:", i.Info()["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 the string representation of the encoding model.
	// It concatenates all the key-value pairs of the Info map, separated by commas.
	// The key is separated from the value by a colon.
	String() string
	// Bytes returns the byte representation of the encoding model.
	// It is a JSON marshalled version of the encoding model.
	//
	Bytes() []byte
}

type FuncInfo

type FuncInfo func() (map[string]interface{}, error)

FuncInfo is a function type that returns dynamic info data and an optional error. The function is called once and the results are cached unless an error occurs.

type FuncName

type FuncName func() (string, error)

FuncName is a function type that returns a dynamic name and an optional error. The function is called once and the result is cached unless an error occurs.

type Info

type Info interface {
	montps.Info

	// RegisterName registers a function that returns a default name.
	// The function must return a string and an error.
	// If the function returns an error, the default name is not registered.
	//
	RegisterName(fct FuncName)
	// RegisterInfo registers a function that returns a default info.
	// The function must return a map of string to interface{} and an error.
	// If the function returns an error, the default info is not registered.
	RegisterInfo(fct FuncInfo)
}

func New

func New(defaultName string) (Info, error)

New creates a new Info instance with the given default name. The default name is used when no dynamic name function is registered or when it fails. Returns an error if the default name is empty.

Example:

info, err := info.New("my-service")
if err != nil {
    log.Fatal(err)
}
fmt.Println(info.Name()) // Output: my-service
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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL