Documentation
¶
Overview ¶
Package introspection provides a domain-agnostic observation layer for Go applications. It enables components to expose their internal state for visualization, monitoring, and debugging without coupling to specific domain concepts.
Core Concepts ¶
The library is built around three pillars:
State Exposure: Components implement simple interfaces to expose their state without domain-specific terminology.
type Introspectable interface { State() any }
type Component interface { ComponentType() string }
Type-Safe Watching: Components can publish state changes through type-safe channels using Go generics.
type TypedWatcher[S any] interface { State() S Watch(ctx context.Context) <-chan StateChange[S] }
Automatic Visualization: Generate Mermaid diagrams from component state with full customization.
config := &DiagramConfig{ SecondaryID: "components", } diagram := TreeDiagram(state, config)
Key Interfaces ¶
The package defines minimal interfaces for maximum flexibility:
- Introspectable: Exposes component state
- Component: Identifies component type
- TypedWatcher[S]: Type-safe state change notifications
- EventSource: Event-based notifications
Visualization ¶
The package includes powerful Mermaid diagram generation:
- TreeDiagram: Hierarchical component structures
- ComponentDiagram: Relationships between component types
- StateMachineDiagram: Component lifecycle and transitions
All visualization is fully customizable through configuration:
config := &DiagramConfig{
PrimaryID: "scheduler",
PrimaryLabel: "Task Scheduler",
PrimaryNodeLabel: "🗓️ Scheduler",
SecondaryID: "tasks",
SecondaryLabel: "Active Tasks",
ConnectionLabel: "schedules",
}
State Aggregation ¶
Combine state changes from multiple components:
watchers := []TypedWatcher[MyState]{component1, component2, component3}
snapshots := AggregateWatchers(ctx, watchers...)
for snapshot := range snapshots {
// Process state changes
}
Design Philosophy ¶
The package emphasizes:
- Domain Agnostic: No hardcoded terminology - you define your domain
- Type Safety: Leverage Go generics for compile-time safety
- Composability: Small, focused interfaces that compose well
- Zero Dependencies: Standard library only
- Backward Compatible: Legacy APIs remain available
Examples ¶
See the examples directory for complete working examples:
- examples/basic: Legacy worker/signal domain
- examples/generic: Domain-agnostic task scheduler
Documentation ¶
For detailed documentation:
- docs/TECHNICAL.md: Architecture and design
- docs/PRODUCT.md: Vision and use cases
- docs/DECISIONS.md: Design rationale
- docs/CONFIGURATION.md: Configuration philosophy
- docs/RECIPES.md: Common usage patterns
Example ¶
Example demonstrates basic usage of the introspection package for observing and visualizing component state.
// Define a simple component state
type ServiceState struct {
Name string
Status string
}
// Create a component that implements TypedWatcher
service := &simpleWatcher[ServiceState]{
state: ServiceState{Name: "API", Status: "Running"},
ch: make(chan introspection.StateChange[ServiceState], 1),
}
// Watch for state changes
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
changes := service.Watch(ctx)
// Trigger a state change
service.UpdateState(ServiceState{Name: "API", Status: "Stopped"})
// Observe the change
select {
case change := <-changes:
fmt.Printf("State changed: %s -> %s\n", change.OldState.Status, change.NewState.Status)
case <-time.After(50 * time.Millisecond):
}
Output: State changed: Running -> Stopped
Index ¶
- Variables
- func AggregateEvents(ctx context.Context, sources ...EventSource) <-chan ComponentEvent
- func AggregateWatchers(ctx context.Context, watchers ...interface{}) <-chan StateSnapshot
- func ComponentDiagram(primary, secondary any, config *DiagramConfig, opts ...MermaidOption) string
- func DefaultStyles() string
- func SignalStateMachine(sig any, opts ...MermaidOption) string
- func StateMachineDiagram(state any, config *StateMachineConfig, opts ...MermaidOption) string
- func SystemDiagram(sig, work any, opts ...MermaidOption) string
- func TreeDiagram(root any, config *DiagramConfig, opts ...MermaidOption) string
- func WorkerTreeDiagram(s any, opts ...MermaidOption) string
- type Component
- type ComponentEvent
- type DiagramConfig
- type EventSource
- type Introspectable
- type MermaidOption
- type MermaidOptions
- type NodeLabelFunc
- type NodeStyleFunc
- type PrimaryNodeLabelFunc
- type PrimaryNodeStyleFunc
- type StateChange
- type StateMachineConfig
- type StateSnapshot
- type TypedWatcher
- type WatcherAdapter
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var Version string
Functions ¶
func AggregateEvents ¶
func AggregateEvents(ctx context.Context, sources ...EventSource) <-chan ComponentEvent
AggregateEvents combines multiple event sources into a unified event stream.
Example ¶
ExampleAggregateEvents demonstrates combining events from multiple event sources into a single stream.
package main
import (
"context"
"fmt"
"time"
introspection "github.com/aretw0/introspection"
)
func main() {
// Create mock event sources
source1 := &mockEventSource{
id: "source-1",
ch: make(chan introspection.ComponentEvent, 1),
}
source2 := &mockEventSource{
id: "source-2",
ch: make(chan introspection.ComponentEvent, 1),
}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
events := introspection.AggregateEvents(ctx, source1, source2)
// Send events from both sources
source1.SendEvent(&mockEvent{id: "source-1", eventType: "started"})
source2.SendEvent(&mockEvent{id: "source-2", eventType: "connected"})
// Collect events
count := 0
for range events {
count++
if count >= 2 {
cancel()
}
}
fmt.Printf("Received %d events\n", count)
}
// mockEventSource is a simple EventSource implementation for examples.
type mockEventSource struct {
id string
ch chan introspection.ComponentEvent
}
func (m *mockEventSource) Events(ctx context.Context) <-chan introspection.ComponentEvent {
out := make(chan introspection.ComponentEvent)
go func() {
defer close(out)
for {
select {
case event := <-m.ch:
select {
case out <- event:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
return out
}
func (m *mockEventSource) SendEvent(event introspection.ComponentEvent) {
m.ch <- event
}
// mockEvent is a simple ComponentEvent implementation for examples.
type mockEvent struct {
id string
eventType string
}
func (e *mockEvent) ComponentID() string {
return e.id
}
func (e *mockEvent) ComponentType() string {
return "service"
}
func (e *mockEvent) Timestamp() time.Time {
return time.Now()
}
func (e *mockEvent) EventType() string {
return e.eventType
}
Output: Received 2 events
func AggregateWatchers ¶
func AggregateWatchers(ctx context.Context, watchers ...interface{}) <-chan StateSnapshot
AggregateWatchers combines multiple typed watchers into a unified snapshot stream.
Example ¶
ExampleAggregateWatchers demonstrates combining state changes from multiple components into a single stream.
type ServiceState struct {
Name string
Status string
}
// Create multiple watchers
service1 := &simpleWatcher[ServiceState]{
id: "service-1",
state: ServiceState{Name: "API", Status: "Running"},
ch: make(chan introspection.StateChange[ServiceState], 1),
}
service2 := &simpleWatcher[ServiceState]{
id: "service-2",
state: ServiceState{Name: "Worker", Status: "Running"},
ch: make(chan introspection.StateChange[ServiceState], 1),
}
// Aggregate state changes from both services
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
snapshots := introspection.AggregateWatchers(ctx, service1, service2)
// Trigger state changes
service1.UpdateState(ServiceState{Name: "API", Status: "Stopped"})
service2.UpdateState(ServiceState{Name: "Worker", Status: "Idle"})
// Collect snapshots
count := 0
for range snapshots {
count++
if count >= 2 {
cancel()
}
}
fmt.Printf("Received %d snapshots\n", count)
Output: Received 2 snapshots
func ComponentDiagram ¶
func ComponentDiagram(primary, secondary any, config *DiagramConfig, opts ...MermaidOption) string
ComponentDiagram renders a customizable topology diagram with two components. This is a generic version that allows full customization of labels and styling.
Example ¶
ExampleComponentDiagram demonstrates creating a diagram showing relationships between two components.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
type Controller struct {
Name string
}
type Worker struct {
ID int
Status string
}
controller := Controller{Name: "MainController"}
worker := Worker{ID: 1, Status: "Active"}
config := introspection.DefaultDiagramConfig()
config.PrimaryID = "controller"
config.PrimaryLabel = "Controller"
config.SecondaryID = "worker"
config.SecondaryLabel = "Worker"
config.ConnectionLabel = "manages"
diagram := introspection.ComponentDiagram(controller, worker, config)
fmt.Println(len(diagram) > 0)
}
Output: true
func DefaultStyles ¶
func DefaultStyles() string
DefaultStyles returns the standard Mermaid class definitions for lifecycle diagrams.
func SignalStateMachine ¶
func SignalStateMachine(sig any, opts ...MermaidOption) string
SignalStateMachine renders a Mermaid state diagram for the signal context. Deprecated: Use StateMachineDiagram with custom StateMachineConfig for domain-agnostic diagrams.
Example ¶
ExampleSignalStateMachine demonstrates the legacy SignalStateMachine function for backward compatibility with the signal domain.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
type SignalState struct {
Enabled bool
Stopping bool
ForceExitThreshold int
}
state := SignalState{
Enabled: true,
ForceExitThreshold: 2,
}
diagram := introspection.SignalStateMachine(state)
fmt.Println(len(diagram) > 0)
}
Output: true
func StateMachineDiagram ¶
func StateMachineDiagram(state any, config *StateMachineConfig, opts ...MermaidOption) string
StateMachineDiagram renders a customizable Mermaid state diagram. It introspects the state object via reflection to find relevant fields.
Example ¶
ExampleStateMachineDiagram demonstrates generating a state machine visualization for a component.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
type ProcessState struct {
Running bool
Stopped bool
}
state := ProcessState{Running: true}
config := introspection.DefaultStateMachineConfig()
config.InitialState = "Idle"
config.GracefulState = "Stopping"
config.ForcedState = "Killed"
config.InitialToGraceful = "STOP"
config.GracefulToForced = "KILL"
config.GracefulToFinal = "Exit"
diagram := introspection.StateMachineDiagram(state, config)
fmt.Println(len(diagram) > 0)
}
Output: true
func SystemDiagram ¶
func SystemDiagram(sig, work any, opts ...MermaidOption) string
SystemDiagram renders a full system topology diagram combining signal context and worker tree. Deprecated: Use ComponentDiagram with custom DiagramConfig for domain-agnostic diagrams. Accepts signal.State and worker.State (or pointers to them) as any.
Example ¶
ExampleSystemDiagram demonstrates the legacy SystemDiagram function that combines signal context and worker tree.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
type SignalState struct {
Enabled bool
Stopping bool
}
type WorkerState struct {
Name string
Status string
Children []WorkerState
}
signal := SignalState{Enabled: true}
worker := WorkerState{
Name: "root",
Status: "Running",
Children: []WorkerState{
{Name: "child-1", Status: "Running"},
},
}
diagram := introspection.SystemDiagram(signal, worker)
fmt.Println(len(diagram) > 0)
}
Output: true
func TreeDiagram ¶
func TreeDiagram(root any, config *DiagramConfig, opts ...MermaidOption) string
TreeDiagram returns a generic Mermaid diagram representing a hierarchical tree structure. The structure is introspected via reflection using common field names (Name, Status, PID, Metadata, Children).
Example ¶
ExampleTreeDiagram demonstrates generating a Mermaid diagram from a hierarchical data structure.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
// Define a tree structure for tasks
type Task struct {
Name string
Status string
Children []Task
}
// Create a task hierarchy
root := Task{
Name: "Project",
Status: "Active",
Children: []Task{
{Name: "Backend", Status: "Running"},
{Name: "Frontend", Status: "Running"},
},
}
// Generate diagram with configuration
config := introspection.DefaultDiagramConfig()
config.SecondaryID = "tasks"
diagram := introspection.TreeDiagram(root, config)
// The diagram contains Mermaid markup
fmt.Println(len(diagram) > 0)
}
Output: true
func WorkerTreeDiagram ¶
func WorkerTreeDiagram(s any, opts ...MermaidOption) string
WorkerTreeDiagram returns a Mermaid diagram string representing the worker hierarchy. Deprecated: Use TreeDiagram with custom DiagramConfig for domain-agnostic diagrams.
Example ¶
ExampleWorkerTreeDiagram demonstrates the legacy WorkerTreeDiagram function for backward compatibility with the worker/signal domain.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
// Define worker state (legacy domain)
type WorkerState struct {
Name string
Status string
PID int
Metadata map[string]string
Children []WorkerState
}
root := WorkerState{
Name: "supervisor",
Status: "Running",
Children: []WorkerState{
{Name: "worker-1", Status: "Running", PID: 1001},
{Name: "worker-2", Status: "Idle", PID: 1002},
},
}
diagram := introspection.WorkerTreeDiagram(root)
fmt.Println(len(diagram) > 0)
}
Output: true
Types ¶
type Component ¶
type Component interface {
// ComponentType returns the type of the component.
ComponentType() string
}
Component identifies the type of a system component. Implementing this interface allows the introspection system to correctly classify the component (e.g., "processor", "controller", "manager") without relying on package paths.
type ComponentEvent ¶
type ComponentEvent interface {
ComponentID() string
ComponentType() string
Timestamp() time.Time
EventType() string
}
ComponentEvent is the interface for event sourcing. Every event must provide identification and timing metadata.
type DiagramConfig ¶
type DiagramConfig struct {
// Primary component configuration
PrimaryID string // Node ID for primary component (default: "primary")
PrimaryLabel string // Subgraph label for primary component (default: "Primary Component")
PrimaryNodeLabel string // Label prefix for primary node (default: "⚡ Component")
// Secondary component configuration
SecondaryID string // Root node ID for secondary component (default: "secondary")
SecondaryLabel string // Subgraph label for secondary component (default: "Secondary Component")
// Connection configuration
ConnectionLabel string // Label for edge between components (default: "manages")
// Node style customization (for secondary/tree nodes)
NodeStyler NodeStyleFunc // Custom function to style nodes based on metadata
NodeLabeler NodeLabelFunc // Custom function to build node labels
// Primary node customization
PrimaryNodeStyler PrimaryNodeStyleFunc // Custom function to determine CSS class for primary component
PrimaryNodeLabeler PrimaryNodeLabelFunc // Custom function to build HTML label for primary component
}
DiagramConfig holds configuration for customizing diagram rendering.
func DefaultDiagramConfig ¶
func DefaultDiagramConfig() *DiagramConfig
DefaultDiagramConfig returns a generic configuration with no domain-specific terms.
type EventSource ¶
type EventSource interface {
// Events returns a channel of component events.
// The channel is closed when the provided context is cancelled.
Events(ctx context.Context) <-chan ComponentEvent
}
EventSource provides an event stream for observability.
type Introspectable ¶
type Introspectable interface {
// State returns a serializable DTO (Data Transfer Object) representing the component's state.
State() any
}
Introspectable is an interface for components that can report their internal state. This is used for generating visualization and status reports.
Note: For type-safe state watching, use TypedWatcher[S] instead.
type MermaidOption ¶
type MermaidOption func(*MermaidOptions)
MermaidOption configures the rendering behavior.
func WithStyles ¶
func WithStyles(styles string) MermaidOption
WithStyles allows custom Mermaid class definitions.
Example ¶
ExampleWithStyles demonstrates customizing Mermaid diagram styles.
package main
import (
"fmt"
introspection "github.com/aretw0/introspection"
)
func main() {
type Task struct {
Name string
Status string
Children []Task
}
root := Task{
Name: "Main",
Status: "Running",
}
customStyles := `
classDef running fill:#90EE90
classDef failed fill:#FFB6C1
`
config := introspection.DefaultDiagramConfig()
diagram := introspection.TreeDiagram(root, config, introspection.WithStyles(customStyles))
fmt.Println(len(diagram) > 0)
}
Output: true
type MermaidOptions ¶
type MermaidOptions struct {
Styles string // Custom Mermaid class definitions
}
MermaidOptions holds Mermaid rendering options.
type NodeLabelFunc ¶
type NodeLabelFunc func(name, status string, pid int, metadata map[string]string, icon string) string
NodeLabelFunc is a function that builds the label for a node.
type NodeStyleFunc ¶
NodeStyleFunc is a function that returns icon, shape start, shape end, and CSS class for a node.
type PrimaryNodeLabelFunc ¶ added in v0.1.3
PrimaryNodeLabelFunc builds the HTML label for the primary component.
type PrimaryNodeStyleFunc ¶ added in v0.1.3
PrimaryNodeStyleFunc determines the CSS class for the primary component based on its state.
type StateChange ¶
type StateChange[S any] struct { ComponentID string ComponentType string // Component type identifier (e.g., "processor", "controller", "manager") OldState S NewState S Timestamp time.Time }
StateChange represents a typed state transition. The generic parameter S allows type-safe access to state without assertions.
type StateMachineConfig ¶
type StateMachineConfig struct {
// State names
InitialState string // Default: "Running"
GracefulState string // Default: "Graceful"
ForcedState string // Default: "ForceExit"
// Transition labels
InitialToGraceful string // Default: "Interrupt"
GracefulToForced string // Default: "Force"
GracefulToFinal string // Default: "Complete"
// Note content generator
NoteGenerator func(state any) string
}
StateMachineConfig configures generic Mermaid state diagram rendering.
func DefaultStateMachineConfig ¶
func DefaultStateMachineConfig() *StateMachineConfig
DefaultStateMachineConfig returns a generic state machine configuration.
type StateSnapshot ¶
type StateSnapshot struct {
ComponentID string
ComponentType string // Component type identifier (e.g., "processor", "controller", "manager")
Timestamp time.Time
Payload any // Component state as any type
}
StateSnapshot is the envelope for cross-domain aggregation. It unifies different state types via a common wrapper.
type TypedWatcher ¶
type TypedWatcher[S any] interface { // State returns the current state snapshot State() S // Watch returns a channel of type-safe state changes. // The channel is closed when the provided context is cancelled. Watch(ctx context.Context) <-chan StateChange[S] }
TypedWatcher provides type-safe state watching for a specific state type S. Implementations can return their domain-specific state without any type assertions.
type WatcherAdapter ¶
type WatcherAdapter[S any] struct { // contains filtered or unexported fields }
WatcherAdapter converts typed state changes to snapshots for aggregation. This allows TypedWatcher[S] instances to participate in cross-domain aggregation.
func NewWatcherAdapter ¶
func NewWatcherAdapter[S any](componentType string, w TypedWatcher[S]) *WatcherAdapter[S]
NewWatcherAdapter creates an adapter for the given typed watcher.
Example ¶
ExampleNewWatcherAdapter demonstrates wrapping a TypedWatcher to convert it to StateSnapshot stream for aggregation.
type ServiceState struct {
Name string
Status string
}
service := &simpleWatcher[ServiceState]{
id: "api",
state: ServiceState{Name: "API", Status: "Running"},
ch: make(chan introspection.StateChange[ServiceState], 1),
}
// Create an adapter
adapter := introspection.NewWatcherAdapter("service", service)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
snapshots := adapter.Snapshots(ctx)
// Trigger a state change
service.UpdateState(ServiceState{Name: "API", Status: "Stopped"})
// Observe the snapshot
select {
case snapshot := <-snapshots:
fmt.Printf("Snapshot from: %s, Type: %s\n", snapshot.ComponentID, snapshot.ComponentType)
case <-time.After(50 * time.Millisecond):
}
Output: Snapshot from: api, Type: service
func (*WatcherAdapter[S]) Snapshots ¶
func (a *WatcherAdapter[S]) Snapshots(ctx context.Context) <-chan StateSnapshot
Snapshots converts the typed state change stream into snapshot envelopes.