Documentation
¶
Overview ¶
Package introspection provides a generic debug variable registry and HTTP server for exposing application internal state over HTTP.
This package is inspired by the standard library's expvar package but extends it with:
- Instance-based registry (not global) for proper lifecycle management
- JSONPath field selection for querying specific fields
- Custom HTTP routing with path-based variable access
The core abstraction is the Var interface, which represents any debug variable that can be queried. Implementations provide their current value via the Get() method.
Example usage:
// Create an instance-scoped registry
registry := introspection.NewRegistry()
// Publish variables
registry.Publish("config", &ConfigVar{provider})
registry.Publish("uptime", introspection.Func(func() (interface{}, error) {
return time.Since(startTime), nil
}))
// Start HTTP server
server := introspection.NewServer(":6060", registry)
server.Start(ctx)
// Query variables:
// GET /debug/vars - list all variables
// GET /debug/vars/config - get config variable
// GET /debug/vars/config?field={.version} - get specific field
Index ¶
- func ExtractField(data interface{}, jsonPathExpr string) (interface{}, error)
- func ParseFieldQuery(r *http.Request) string
- func WriteError(w http.ResponseWriter, code int, message string)
- func WriteJSON(w http.ResponseWriter, data interface{})
- func WriteJSONField(w http.ResponseWriter, data interface{}, field string)
- func WriteJSONWithStatus(w http.ResponseWriter, statusCode int, data interface{})
- func WriteText(w http.ResponseWriter, text string)
- type ComponentHealth
- type FloatVar
- type Func
- type HealthCheckFunc
- type IntVar
- type MapVar
- type Registry
- func (r *Registry) All() (map[string]interface{}, error)
- func (r *Registry) Clear()
- func (r *Registry) Get(path string) (interface{}, error)
- func (r *Registry) GetWithField(path, field string) (interface{}, error)
- func (r *Registry) Len() int
- func (r *Registry) Paths() []string
- func (r *Registry) Publish(path string, v Var)
- type Server
- type StringVar
- type TypedFunc
- type TypedGetter
- type TypedVar
- type Var
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ExtractField ¶
ExtractField extracts a specific field from data using JSONPath syntax.
Uses k8s.io/client-go/util/jsonpath (kubectl-style JSONPath) for field extraction. This provides familiar syntax for Kubernetes users.
JSONPath syntax examples:
- {.version} - simple field
- {.config.templates} - nested field
- {.items[0]} - array index
- {.items[*].name} - all names from array
The data parameter should be JSON-serializable. The jsonPathExpr parameter should include braces (e.g., "{.version}").
Example:
config := map[string]interface{}{
"version": "1.2.3",
"templates": map[string]string{"main": "..."},
}
version, err := ExtractField(config, "{.version}")
// Returns: "1.2.3"
templates, err := ExtractField(config, "{.templates}")
// Returns: map[string]string{"main": "..."}
func ParseFieldQuery ¶
ParseFieldQuery extracts the "field" query parameter from an HTTP request.
Returns the field parameter value if present, or empty string otherwise.
Example:
// GET /debug/vars/config?field={.version}
field := ParseFieldQuery(r) // Returns: "{.version}"
// GET /debug/vars/config
field := ParseFieldQuery(r) // Returns: ""
func WriteError ¶
func WriteError(w http.ResponseWriter, code int, message string)
WriteError writes an error response with the specified HTTP status code.
The error message is wrapped in a JSON object with an "error" field.
Example:
WriteError(w, http.StatusNotFound, "variable not found")
// Response: {"error": "variable not found"}
func WriteJSON ¶
func WriteJSON(w http.ResponseWriter, data interface{})
WriteJSON writes data as JSON to the HTTP response with status 200 OK.
Sets appropriate Content-Type header and handles JSON marshaling. If marshaling fails, writes an error response instead.
Example:
WriteJSON(w, map[string]interface{}{
"status": "ok",
"count": 42,
})
func WriteJSONField ¶
func WriteJSONField(w http.ResponseWriter, data interface{}, field string)
WriteJSONField writes a specific field from data as JSON using JSONPath.
The field parameter should use kubectl-style JSONPath syntax (e.g., "{.version}"). If field is empty, writes the entire data object.
Example:
config := map[string]interface{}{
"version": "1.2.3",
"templates": []string{"main", "secondary"},
}
// Get full object
WriteJSONField(w, config, "")
// Get specific field
WriteJSONField(w, config, "{.version}") // Returns: "1.2.3"
func WriteJSONWithStatus ¶
func WriteJSONWithStatus(w http.ResponseWriter, statusCode int, data interface{})
WriteJSONWithStatus writes data as JSON with a custom HTTP status code.
Use this when you need to return JSON with a non-200 status code.
Example:
WriteJSONWithStatus(w, http.StatusServiceUnavailable, map[string]interface{}{
"status": "degraded",
"components": components,
})
func WriteText ¶
func WriteText(w http.ResponseWriter, text string)
WriteText writes a plain text response.
Useful for simple string responses or formatted text.
Example:
WriteText(w, "OK\n")
Types ¶
type ComponentHealth ¶
ComponentHealth represents the health status of a single component.
type FloatVar ¶
type FloatVar struct {
// contains filtered or unexported fields
}
FloatVar is a thread-safe float64 variable.
Example:
temperature := NewFloat(0.0)
registry.Publish("cpu_temperature", temperature)
temperature.Set(45.2)
type Func ¶
type Func func() (interface{}, error)
Func is a Var that computes its value on-demand by calling a function.
This is useful for values that are expensive to compute or change frequently, as they are only calculated when actually queried.
Example:
startTime := time.Now()
registry.Publish("uptime", Func(func() (interface{}, error) {
return time.Since(startTime).String(), nil
}))
type HealthCheckFunc ¶
type HealthCheckFunc func() map[string]ComponentHealth
HealthCheckFunc is a function that returns component health status. Used to integrate with lifecycle registry or other health monitoring.
type IntVar ¶
type IntVar struct {
// contains filtered or unexported fields
}
IntVar is a thread-safe 64-bit integer variable.
Provides atomic operations for reading and modifying the value. Similar to expvar.Int but instance-based rather than global.
Example:
counter := NewInt(0)
registry.Publish("request_count", counter)
counter.Add(1) // Increment counter
type MapVar ¶
type MapVar struct {
// contains filtered or unexported fields
}
MapVar is a thread-safe map variable.
Useful for publishing structured data that changes over time.
Example:
stats := NewMap()
registry.Publish("stats", stats)
stats.Set("requests", 100)
stats.Set("errors", 5)
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry manages a collection of debug variables.
Unlike expvar which uses a global registry, this implementation is instance-based, allowing for proper lifecycle management. Each Registry instance is independent and can be garbage collected when no longer needed.
This is especially important for applications that reinitialize components (like a Kubernetes controller that reloads configuration), as it prevents stale references to old component instances.
Registry is thread-safe and can be accessed from multiple goroutines.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry creates a new empty registry.
Each registry instance is independent and manages its own set of variables.
Example:
registry := introspection.NewRegistry()
func (*Registry) All ¶
All returns all variables as a map of path → value.
If any variable's Get() method fails, the error is returned and the map may be incomplete.
This is used by the /debug/vars endpoint to list all variables.
Example:
all, err := registry.All()
for path, value := range all {
fmt.Printf("%s = %v\n", path, value)
}
func (*Registry) Clear ¶
func (r *Registry) Clear()
Clear removes all published variables from the registry.
This is used between controller iterations to prevent stale references to components from previous iterations. The registry can then be reused with new variables without restarting the HTTP server.
Example:
// Between iterations
registry.Clear()
registry.Publish("config", newConfigVar)
func (*Registry) Get ¶
Get retrieves the value of the variable at the specified path.
Returns an error if the path does not exist or if the variable's Get() method fails.
Example:
value, err := registry.Get("config")
if err != nil {
slog.Error("Failed to get config", "error", err)
}
func (*Registry) GetWithField ¶
GetWithField retrieves a specific field from the variable at the specified path using JSONPath syntax.
The field parameter should use kubectl-style JSONPath syntax (e.g., "{.version}"). If field is empty, the full variable value is returned.
This method is used internally by the HTTP handlers to support field selection via query parameters.
Example:
// Get full config
value, err := registry.GetWithField("config", "")
// Get just the version field
version, err := registry.GetWithField("config", "{.version}")
func (*Registry) Len ¶
Len returns the number of registered variables.
Example:
count := registry.Len() // Returns number of published variables
func (*Registry) Paths ¶
Paths returns a sorted list of all registered variable paths.
This is used by the /debug/vars endpoint to provide an index of available variables.
Example:
paths := registry.Paths() // Returns: ["config", "resources/ingresses", "uptime"]
func (*Registry) Publish ¶
Publish registers a variable at the specified path.
The path is used to access the variable via HTTP (e.g., /debug/vars/{path}). If a variable already exists at the given path, it is replaced.
Path format:
- Use simple names: "config", "uptime", "stats"
- Or hierarchical paths: "resources/ingresses", "cache/hits"
Example:
registry.Publish("config", &ConfigVar{provider})
registry.Publish("resources/ingresses", &IngressVar{store})
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves debug variables over HTTP.
The server provides HTTP endpoints for accessing variables registered in a Registry. It supports JSONPath field selection for querying specific fields from variables.
Standard endpoints:
- GET /debug/vars - list all variable paths
- GET /debug/vars/all - get all variables
- GET /debug/vars/{path} - get specific variable
- GET /debug/vars/{path}?field={.jsonpath} - get field from variable
- GET /health - health check
- GET /debug/pprof/* - Go profiling endpoints (via import side-effect)
Custom handlers can be registered using RegisterHandler() before Setup() is called.
The server supports two initialization patterns:
1. Simple (combined): Use Start() which calls Setup() and Serve() internally:
go server.Start(ctx)
2. Two-phase (for early health checks): Call Setup() first, then Serve() later:
server.RegisterHandler("/debug/events", eventsHandler)
server.SetHealthChecker(checker)
server.Setup()
go server.Serve(ctx)
The two-phase pattern allows registering handlers and health checkers before routes are finalized, while enabling the server to start serving immediately.
func NewServer ¶
NewServer creates a new HTTP server for serving debug variables.
Parameters:
- addr: TCP address to listen on (e.g., ":6060" or "localhost:6060")
- registry: The variable registry to serve
Example:
registry := introspection.NewRegistry()
registry.Publish("config", &ConfigVar{provider})
server := introspection.NewServer(":6060", registry)
go server.Start(ctx)
func (*Server) RegisterHandler ¶
func (s *Server) RegisterHandler(pattern string, handler http.HandlerFunc)
RegisterHandler registers a custom HTTP handler for the given pattern. This must be called before Setup() (or Start(), which calls Setup() internally).
Parameters:
- pattern: URL pattern (e.g., "/debug/events")
- handler: HTTP handler function
Example:
server.RegisterHandler("/debug/events", func(w http.ResponseWriter, r *http.Request) {
correlationID := r.URL.Query().Get("correlation_id")
events := commentator.FindByCorrelationID(correlationID, 100)
introspection.WriteJSON(w, events)
})
func (*Server) Serve ¶
Serve starts the HTTP server and blocks until the context is cancelled. Setup() must be called before Serve().
This method should typically be run in a goroutine:
server.Setup() go server.Serve(ctx)
The server performs graceful shutdown when the context is cancelled, waiting for active connections to complete (up to a timeout).
Example (two-phase initialization for early health checks):
server.RegisterHandler("/debug/events", eventsHandler)
server.SetHealthChecker(checker)
server.Setup()
go server.Serve(ctx)
func (*Server) SetHealthChecker ¶
func (s *Server) SetHealthChecker(checker HealthCheckFunc)
SetHealthChecker sets a function to check component health. This must be called before Start().
The health checker function is called by the /health endpoint to get the health status of all components. If not set, /health just returns "ok".
Example integration with lifecycle registry:
server.SetHealthChecker(func() map[string]introspection.ComponentHealth {
status := registry.Status()
result := make(map[string]introspection.ComponentHealth)
for name, info := range status {
healthy := info.Status == lifecycle.StatusRunning
if info.Healthy != nil {
healthy = *info.Healthy
}
result[name] = introspection.ComponentHealth{
Healthy: healthy,
Error: info.Error,
}
}
return result
})
func (*Server) Setup ¶
func (s *Server) Setup()
Setup initializes routes and prepares the server for serving. This must be called before Serve(). Custom handlers and health checker can be registered after NewServer() but before Setup().
After Setup() is called, no new handlers can be registered.
Example:
server.RegisterHandler("/debug/events", eventsHandler)
server.SetHealthChecker(checker)
server.Setup()
go server.Serve(ctx)
func (*Server) Start ¶
Start is a convenience method that calls Setup() then Serve(). For more control over timing (e.g., registering handlers after server creation but before routes are finalized), call Setup() and Serve() separately.
This method should typically be run in a goroutine:
go server.Start(ctx)
Example:
ctx, cancel := context.WithCancel(context.Background()) defer cancel() go server.Start(ctx) // Later: cancel context to shutdown cancel()
type StringVar ¶
type StringVar struct {
// contains filtered or unexported fields
}
StringVar is a thread-safe string variable.
Example:
status := NewString("initializing")
registry.Publish("status", status)
status.Set("running")
type TypedFunc ¶
TypedFunc is a generic adapter that wraps a typed function as a Var.
This allows creating type-safe debug variables while still implementing the Var interface. The type parameter T specifies the return type of the underlying function.
Example:
// Create a typed var that returns a specific type
getStats := func() (Stats, error) {
return Stats{Count: 42, LastUpdate: time.Now()}, nil
}
statsVar := TypedFunc[Stats](getStats)
registry.Publish("stats", statsVar)
type TypedGetter ¶
TypedGetter is a generic interface for typed variable access.
Unlike Var which returns interface{}, TypedGetter returns a specific type. This is useful for internal code that knows the expected type.
type TypedVar ¶
type TypedVar[T any] struct { // contains filtered or unexported fields }
TypedVar wraps a TypedGetter to implement both Var and TypedGetter interfaces.
func NewTypedVar ¶
NewTypedVar creates a new TypedVar from a getter function.
type Var ¶
type Var interface {
// Get returns the current value of this variable.
//
// The returned value should be JSON-serializable.
// If an error occurs while retrieving the value, it should be returned.
//
// Implementations must be thread-safe, as Get() may be called concurrently
// from multiple HTTP requests.
Get() (interface{}, error)
}
Var represents a debug variable that can be queried for its current value.
Implementations should return their current state when Get() is called. The returned value must be JSON-serializable.
Example implementation:
type ConfigVar struct {
mu sync.RWMutex
config *Config
}
func (v *ConfigVar) Get() (interface{}, error) {
v.mu.RLock()
defer v.mu.RUnlock()
return v.config, nil
}