Documentation
¶
Overview ¶
Package fields provides a thread-safe, context-aware structured logging fields management system that seamlessly integrates with logrus and Go's standard context package.
Design Philosophy ¶
The fields package is designed around three core principles:
1. Context Integration: Full implementation of context.Context interface for seamless integration with Go's cancellation and deadline propagation mechanisms.
2. Type Safety: Generic-based implementation ensuring type-safe operations while maintaining flexibility for any value type through interface{}.
3. Immutability Options: Support for both mutable operations (Add, Delete, Map) and immutable patterns (Clone) to suit different use cases.
Architecture ¶
The package consists of three main components:
1. Fields Interface: Public API providing logging field operations, context methods, and JSON serialization capabilities.
2. fldModel Implementation: Internal structure wrapping github.com/nabbar/golib/context.Config[string] for thread-safe storage and retrieval of key-value pairs.
3. Integration Layer: Bidirectional conversion between Fields and logrus.Fields for compatibility with existing logging infrastructure.
Key Features ¶
Thread Safety: Read operations (Get, Logrus, Walk) are thread-safe for concurrent access. Single write operations (Add, Store, Delete, LoadOrStore, LoadAndDelete) are also thread-safe thanks to the underlying sync.Map implementation. Composite operations (Map, Merge, Clean) require external synchronization if used concurrently from multiple goroutines.
Context Propagation: Full context.Context implementation allows Fields to participate in Go's cancellation and deadline mechanisms, enabling proper cleanup in long-running operations.
JSON Serialization: Built-in JSON marshaling and unmarshaling support for persistence, network transmission, or configuration storage.
Flexible Operations:
- Add/Store: Insert or update key-value pairs
- Get/LoadOrStore: Retrieve values with optional defaults
- Delete/LoadAndDelete: Remove entries with optional retrieval
- Walk/WalkLimit: Iterate over entries with filtering support
- Map: Transform all values using a custom function
- Merge: Combine multiple Fields instances
Logrus Integration: Direct conversion to/from logrus.Fields for zero-friction integration with existing logrus-based logging systems.
Performance Characteristics ¶
Memory Usage: O(n) where n is the number of fields stored. Each field requires storage for both key (string) and value (interface{}). The underlying sync.Map implementation adds minimal overhead.
Time Complexity:
- Add, Get, Delete: O(1) average case (map operations)
- Walk, WalkLimit: O(n) where n is number of fields
- Map: O(n) with additional transformation cost
- Clone: O(n) deep copy operation
- Logrus: O(n) conversion to logrus.Fields
- Merge: O(m) where m is number of fields in source
Thread Safety: Lock-free reads with atomic operations. Writes use appropriate synchronization mechanisms provided by the underlying context.Config implementation.
Use Cases ¶
Structured Logging: Create field sets for structured log entries with consistent formatting and easy transformation.
flds := fields.New(ctx)
flds.Add("request_id", "abc123")
flds.Add("user_id", 42)
flds.Add("action", "login")
logger.WithFields(flds.Logrus()).Info("User action")
Request Context Enrichment: Attach metadata to request contexts for distributed tracing and debugging.
flds := fields.New(r.Context())
flds.Add("trace_id", traceID)
flds.Add("span_id", spanID)
newCtx := context.WithValue(r.Context(), "fields", flds)
Field Transformation: Apply transformations to all field values for sanitization, formatting, or encoding.
flds.Map(func(key string, val interface{}) interface{} {
if key == "password" {
return "[REDACTED]"
}
return val
})
Multi-Source Aggregation: Combine fields from multiple sources while maintaining immutability of originals.
base := fields.New(ctx).Add("service", "api")
request := base.Clone().Add("method", "POST")
response := request.Clone().Add("status", 200)
Configuration Serialization: Store and retrieve field configurations using JSON.
data, _ := json.Marshal(flds) restored := fields.New(ctx) json.Unmarshal(data, restored)
Limitations and Considerations ¶
Context Lifetime: Fields instances hold a reference to their context. Ensure proper context cancellation to avoid resource leaks in long-running applications.
Value Types: While any type can be stored via interface{}, complex types (structs, pointers) should be used carefully. JSON serialization may not preserve all type information.
Nil Handling: The package handles nil Fields instances gracefully, returning nil or empty results. However, this behavior should not be relied upon in production code. Always check for nil before operations.
Deep Copy Behavior: Clone() performs a deep copy of the internal map but does not deep copy the values themselves. If values are pointers or references, modifications to the underlying data will affect all clones.
logrus.Fields Conversion: The Logrus() method creates a new map on each call. For performance-critical code, cache the result if multiple accesses are needed.
Walk Function Behavior: Walk and WalkLimit continue iteration until the callback returns false or all entries are processed. The iteration order is not guaranteed due to the underlying map implementation.
Best Practices ¶
Prefer Clone for Immutability: When creating derived field sets, use Clone() to avoid unintended modifications to the original.
derived := original.Clone().Add("extra", "field")
Use WalkLimit for Selective Operations: When only specific fields are needed, use WalkLimit to improve performance.
flds.WalkLimit(func(key string, val interface{}) bool {
// Process only specified keys
return true
}, "request_id", "trace_id")
Context-Aware Cleanup: Always pass an appropriate context to New() for proper lifecycle management.
ctx, cancel := context.WithTimeout(parent, 5*time.Second) defer cancel() flds := fields.New(ctx)
Avoid Storing Large Values: Fields are designed for metadata, not data storage. Keep values small and reference larger data structures by ID or key.
Type Assertions with Care: When retrieving values, always check type assertions to avoid panics.
if val, ok := flds.Get("key"); ok {
if str, ok := val.(string); ok {
// Use str safely
}
}
Thread Safety Model ¶
The Fields implementation provides strong thread-safety guarantees:
Thread-Safe Operations (no synchronization needed):
- Read operations: Get, Logrus, Walk, WalkLimit
- Single write operations: Add, Store, Delete, LoadOrStore, LoadAndDelete
- Clone() creates independent instances safe for parallel modification
Composite Operations (require external synchronization if concurrent):
- Map: iterates and modifies all values
- Merge: iterates source and stores in receiver
- Clean: removes all entries
Safe Concurrent Patterns:
// Multiple goroutines can safely call Add/Store/Delete
go func() { flds.Add("key1", "value1") }()
go func() { flds.Add("key2", "value2") }()
// For composite operations, use Clone per goroutine
go func() {
local := flds.Clone()
local.Map(transformFunc)
}()
Examples ¶
See the example_test.go file for comprehensive examples demonstrating:
- Basic field creation and manipulation
- Integration with logrus logger
- Context propagation and cancellation
- JSON serialization and deserialization
- Field transformation with Map
- Merging multiple field sources
- Walking and filtering operations
Related Packages ¶
- github.com/nabbar/golib/context: Underlying context management
- github.com/sirupsen/logrus: Logging framework integration
- encoding/json: Standard JSON serialization
Versioning and Compatibility ¶
This package follows semantic versioning. The API is stable and backwards compatible within major versions. Breaking changes will only occur in major version increments.
Minimum Go version: 1.18 (requires generics support)
License ¶
MIT License - See LICENSE file for details.
Copyright (c) 2025 Nicolas JUHEL
Example ¶
Example demonstrates a typical usage pattern combining multiple operations.
package main
import (
"context"
"fmt"
"github.com/nabbar/golib/logger/fields"
)
func main() {
// Create base fields
flds := fields.New(context.Background())
// Add fields with chaining
flds.Add("service", "api").
Add("version", "1.0").
Add("env", "prod")
// Clone for specific request
reqFields := flds.Clone()
reqFields.Add("request_id", "12345")
// Get logrus-compatible fields
logFields := reqFields.Logrus()
fmt.Println(len(logFields))
}
Output: 4
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Fields ¶
type Fields interface {
context.Context
json.Marshaler
json.Unmarshaler
// Clone creates a deep copy of the Fields instance.
//
// The returned Fields instance is completely independent and modifications to either
// the original or the clone will not affect the other. This is essential for creating
// derived field sets without side effects.
//
// Note: While the internal map is deep copied, the values themselves are not. If values
// are pointers or references, modifications to the underlying data will affect all clones.
//
// Returns nil if the receiver is nil.
//
// Example:
// base := fields.New(ctx).Add("service", "api")
// request := base.Clone().Add("request_id", "123")
// // base still has only "service" field
Clone() Fields
// Clean removes all key-value pairs from the Fields instance.
//
// This is useful for resetting a Fields instance to empty state while preserving
// the underlying context. After calling Clean(), the Fields instance can be reused.
//
// This is a composite operation that requires external synchronization if used
// concurrently with other operations.
Clean()
// Add inserts or updates a key-value pair in the Fields instance.
//
// If the key already exists, its value is overwritten with the new value.
// If the key does not exist, a new key-value pair is added.
//
// The method returns the same Fields instance to enable method chaining.
// This operation is thread-safe and can be called concurrently from multiple goroutines.
//
// Any type can be stored as a value via interface{}, but consider JSON serialization
// compatibility if persistence is needed.
//
// Example:
// flds.Add("key1", "value1").Add("key2", 42).Add("key3", true)
Add(key string, val interface{}) Fields
// Delete removes the key-value pair associated with the given key.
//
// If the key does not exist, this is a no-op (no error is returned).
// The method returns the same Fields instance to enable method chaining.
// This operation is thread-safe.
//
// Example:
// flds.Delete("temp_key").Delete("another_key")
Delete(key string) Fields
// Merge combines all key-value pairs from the source Fields into the receiver.
//
// For keys that exist in both Fields instances, the source value overwrites the
// receiver's value. The source Fields instance is not modified.
//
// This is a composite operation that requires external synchronization if used
// concurrently with other operations.
//
// Returns the receiver to enable method chaining.
// Returns the receiver unchanged if source is nil.
//
// Example:
// base.Merge(extra) // Adds all fields from extra to base
Merge(f Fields) Fields
// Walk iterates over all key-value pairs, calling the provided function for each pair.
//
// The iteration continues until either:
// - All pairs have been visited
// - The callback function returns false
//
// The iteration order is not guaranteed due to the underlying map implementation.
//
// Returns the receiver to enable method chaining.
//
// Example:
// flds.Walk(func(key string, val interface{}) bool {
// fmt.Printf("%s: %v\n", key, val)
// return true // Continue iteration
// })
Walk(fct libctx.FuncWalk[string]) Fields
// WalkLimit iterates only over the specified keys, calling the provided function for each.
//
// Only the keys listed in validKeys will be visited. If a listed key does not exist,
// it is silently skipped. This is more efficient than Walk when only specific fields
// are needed.
//
// Returns the receiver to enable method chaining.
//
// Example:
// flds.WalkLimit(func(key string, val interface{}) bool {
// // Only processes "request_id" and "trace_id"
// return true
// }, "request_id", "trace_id")
WalkLimit(fct libctx.FuncWalk[string], validKeys ...string) Fields
// Get retrieves the value associated with the given key.
//
// Returns the value and true if the key exists, or nil and false if it does not.
// This follows the standard Go map access pattern.
//
// Example:
// if val, ok := flds.Get("key"); ok {
// // Use val safely
// }
Get(key string) (val interface{}, ok bool)
// Store inserts or updates a key-value pair without returning the Fields instance.
//
// This method is similar to Add() but doesn't return the Fields instance, making it
// suitable for use when method chaining is not needed. It's a direct storage operation.
//
// This operation is thread-safe and can be called concurrently from multiple goroutines
// thanks to the underlying sync.Map implementation.
//
// Example:
// flds.Store("config_key", configValue)
// flds.Store("timestamp", time.Now())
Store(key string, cfg interface{})
// LoadOrStore atomically retrieves or stores a value for the given key.
//
// If the key exists, returns the existing value and true.
// If the key does not exist, stores the provided value and returns it with false.
//
// This is useful for lazy initialization patterns where a default value should be
// set only if the key doesn't already exist.
//
// Returns:
// - val: The existing value if loaded=true, or the stored value if loaded=false
// - loaded: true if the key existed, false if the value was stored
//
// Example:
// val, loaded := flds.LoadOrStore("counter", 0)
// if !loaded {
// // First access, counter was initialized to 0
// }
LoadOrStore(key string, cfg interface{}) (val interface{}, loaded bool)
// LoadAndDelete atomically retrieves and removes a value for the given key.
//
// If the key exists, returns the value and true, and the key is deleted.
// If the key does not exist, returns nil and false.
//
// This is useful for one-time operations or cleanup scenarios.
//
// Returns:
// - val: The value if it existed, nil otherwise
// - loaded: true if the key existed and was deleted, false otherwise
//
// Example:
// if val, existed := flds.LoadAndDelete("temp"); existed {
// // Process val, field is now deleted
// }
LoadAndDelete(key string) (val interface{}, loaded bool)
// Logrus returns the logrus.Fields instance associated with the current Fields instance.
//
// This method is useful when you want to directly access the logrus.Fields instance
// associated with the current Fields instance.
//
// The returned logrus.Fields instance is a reference to the same instance as the one
// associated with the current Fields instance. Any modification to the returned logrus.Fields
// instance will affect the Fields instance.
//
// The returned logrus.Fields instance is valid until the Fields instance is modified or
// until the Fields instance is garbage collected.
//
// If the Fields instance is nil, this method will return nil.
Logrus() logrus.Fields
// Map applies a transformation function to all key-value pairs in the Fields instance.
//
// The transformation function is called for each key-value pair. It takes the key
// and value as arguments, and returns the new value to store.
//
// This is a composite operation that requires external synchronization if used
// concurrently with other operations. The transformation is applied in-place.
//
// Example:
// flds.Map(func(key string, val interface{}) interface{} {
// if key == "password" {
// return "[REDACTED]"
// }
// return val
// })
Map(fct func(key string, val interface{}) interface{}) Fields
}
Fields provides a thread-safe, context-aware structured logging fields management interface.
This interface combines three key capabilities:
- context.Context implementation for lifecycle management and cancellation propagation
- json.Marshaler/Unmarshaler for serialization and persistence
- Key-value storage with various access patterns
Thread Safety:
- Read operations (Get, Logrus, Walk) are thread-safe for concurrent access
- Single write operations (Add, Store, Delete, LoadOrStore, LoadAndDelete) are thread-safe thanks to the underlying sync.Map implementation
- Composite operations (Map, Merge, Clean) require external synchronization if used concurrently
- For concurrent composite operations, use Clone() to create independent instances per goroutine
Context Integration: Fields fully implements context.Context, allowing it to participate in Go's cancellation and deadline mechanisms. The context provided to New() determines the lifecycle of the Fields instance.
See example_test.go for comprehensive usage examples.
Example (ComplexTypes) ¶
ExampleFields_complexTypes demonstrates handling complex data types.
package main
import (
"fmt"
"github.com/nabbar/golib/logger/fields"
)
func main() {
flds := fields.New(nil)
// Add various types
flds.Add("string", "text")
flds.Add("int", 42)
flds.Add("float", 3.14)
flds.Add("bool", true)
flds.Add("slice", []int{1, 2, 3})
flds.Add("map", map[string]string{"key": "value"})
fmt.Printf("Total fields: %d\n", len(flds.Logrus()))
}
Output: Total fields: 6
Example (Context) ¶
ExampleFields_context demonstrates context integration. This shows how Fields implements context.Context interface.
package main
import (
"context"
"fmt"
"github.com/nabbar/golib/logger/fields"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
flds := fields.New(ctx)
flds.Add("trace_id", "xyz789")
// Fields implements context.Context
select {
case <-flds.Done():
fmt.Println("Context cancelled")
default:
fmt.Println("Context active")
}
}
Output: Context active
Example (MultiSourceAggregation) ¶
ExampleFields_multiSourceAggregation demonstrates combining fields from multiple sources. This shows a real-world scenario of aggregating metadata from different components.
package main
import (
"fmt"
"github.com/nabbar/golib/logger/fields"
)
func main() {
// System-level fields
sysFields := fields.New(nil)
sysFields.Add("hostname", "server-01")
sysFields.Add("pid", 12345)
// Application-level fields
appFields := fields.New(nil)
appFields.Add("app_name", "auth-service")
appFields.Add("app_version", "3.0.0")
// Request-level fields
reqFields := fields.New(nil)
reqFields.Add("request_id", "req-xyz")
reqFields.Add("user_agent", "Mozilla/5.0")
// Merge all sources
combined := sysFields.Clone()
combined.Merge(appFields)
combined.Merge(reqFields)
fmt.Printf("Combined fields: %d\n", len(combined.Logrus()))
}
Output: Combined fields: 6
Example (StructuredLogging) ¶
ExampleFields_structuredLogging demonstrates a complete structured logging workflow. This is a comprehensive example combining multiple features.
package main
import (
"context"
"fmt"
"github.com/nabbar/golib/logger/fields"
)
func main() {
// Initialize base fields for the service
baseFields := fields.New(context.Background())
baseFields.Add("service", "user-api")
baseFields.Add("version", "2.1.0")
baseFields.Add("environment", "production")
// Create request-specific fields by cloning
requestFields := baseFields.Clone()
requestFields.Add("request_id", "req-abc-123")
requestFields.Add("method", "POST")
requestFields.Add("path", "/api/users")
// Transform sensitive data
requestFields.Map(func(key string, val interface{}) interface{} {
if key == "password" {
return "[REDACTED]"
}
return val
})
// Convert to logrus format for logging
logrusFields := requestFields.Logrus()
fmt.Printf("Total fields: %d\n", len(logrusFields))
fmt.Printf("Has service: %v\n", logrusFields["service"] != nil)
fmt.Printf("Has request_id: %v\n", logrusFields["request_id"] != nil)
}
Output: Total fields: 6 Has service: true Has request_id: true
func New ¶
New creates a new Fields instance from the given context.Context.
It returns a new Fields instance which is associated with the given context.Context. The returned Fields instance can be used to add, remove, or modify key/val pairs.
If the given context.Context is nil, this method will return nil.
Example usage:
flds := New(context.Background)
flds.Add("key", "value")
flds.Map(func(key string, val interface{}) interface{} {
return fmt.Sprintf("%s-%s", key, val)
})
The above example shows how to create a new Fields instance from a context.Context, and how to use the returned Fields instance to add, remove, or modify key/val pairs.
Example ¶
ExampleNew demonstrates basic Fields creation. This is the simplest use case for creating a new Fields instance.
package main
import (
"context"
"fmt"
"github.com/nabbar/golib/logger/fields"
)
func main() {
// Create a new Fields instance with background context
flds := fields.New(context.Background())
// Add a simple field
flds.Add("message", "hello")
// Get logrus compatible fields
fmt.Println(len(flds.Logrus()))
}
Output: 1