The plugin system allows you to extend interpreter behavior with lifecycle hooks without modifying core code.
Plugins implement one or more hook interfaces to receive callbacks at specific points in the interpreter lifecycle:
- Interpreter lifecycle:
OnStart,OnStop - Event processing:
OnEvent(can modify events) - State transitions:
OnEnter,OnExit,BeforeTransition,AfterTransition - Action execution:
BeforeAction,AfterAction - Error handling:
OnError
import "go.klarlabs.de/statekit/plugin"
// Create a plugin by implementing the hooks you need
type LoggingPlugin[C any] struct {
logger *slog.Logger
}
func (p *LoggingPlugin[C]) Name() string {
return "logging"
}
func (p *LoggingPlugin[C]) OnStart(ctx plugin.Context[C]) {
p.logger.Info("interpreter started")
}
func (p *LoggingPlugin[C]) OnStop(ctx plugin.Context[C]) {
p.logger.Info("interpreter stopped")
}
func (p *LoggingPlugin[C]) OnEnter(ctx plugin.Context[C], state plugin.StateID) {
p.logger.Info("entered state", "state", state)
}
func (p *LoggingPlugin[C]) OnExit(ctx plugin.Context[C], state plugin.StateID) {
p.logger.Info("exited state", "state", state)
}
// Register with interpreter
interp := statekit.NewInterpreter(machine)
interp.Use(&LoggingPlugin[MyContext]{logger: slog.Default()})
interp.Start()All plugins must implement the base Plugin interface:
type Plugin[C any] interface {
Name() string
}type OnStartPlugin[C any] interface {
Plugin[C]
OnStart(ctx Context[C])
}
type OnStopPlugin[C any] interface {
Plugin[C]
OnStop(ctx Context[C])
}The event hook can intercept and modify events before processing:
type OnEventPlugin[C any] interface {
Plugin[C]
OnEvent(ctx Context[C], event Event) Event
}Example - adding metadata to events:
func (p *AuditPlugin[C]) OnEvent(ctx plugin.Context[C], event plugin.Event) plugin.Event {
event.Payload = map[string]any{
"original": event.Payload,
"timestamp": time.Now(),
"user": ctx.MachineContext.UserID,
}
return event
}type OnStatePlugin[C any] interface {
Plugin[C]
OnEnter(ctx Context[C], state StateID)
OnExit(ctx Context[C], state StateID)
}type OnTransitionPlugin[C any] interface {
Plugin[C]
BeforeTransition(ctx Context[C], from, to StateID, event Event)
AfterTransition(ctx Context[C], from, to StateID, event Event)
}type OnActionPlugin[C any] interface {
Plugin[C]
BeforeAction(ctx Context[C], action ActionType, event Event)
AfterAction(ctx Context[C], action ActionType, event Event)
}type OnErrorPlugin[C any] interface {
Plugin[C]
OnError(ctx Context[C], err error)
}All hooks receive a plugin.Context[C] with access to interpreter state:
type Context[C any] struct {
MachineID string
MachineContext C
CurrentState StateID
}Use Composite to combine multiple plugins:
loggingPlugin := &LoggingPlugin[MyContext]{}
metricsPlugin := &MetricsPlugin[MyContext]{}
auditPlugin := &AuditPlugin[MyContext]{}
composite := plugin.NewComposite[MyContext](loggingPlugin, metricsPlugin, auditPlugin)
interp.Use(composite)Plugins are called in registration order.
You can also register plugins individually:
interp := statekit.NewInterpreter(machine)
interp.Use(loggingPlugin)
interp.Use(metricsPlugin)
interp.Use(auditPlugin)type MetricsPlugin[C any] struct {
transitionCounter prometheus.Counter
stateGauge prometheus.GaugeVec
}
func (p *MetricsPlugin[C]) Name() string {
return "metrics"
}
func (p *MetricsPlugin[C]) AfterTransition(ctx plugin.Context[C], from, to plugin.StateID, event plugin.Event) {
p.transitionCounter.Inc()
p.stateGauge.WithLabelValues(string(to)).Set(1)
p.stateGauge.WithLabelValues(string(from)).Set(0)
}type AuditPlugin[C any] struct {
store AuditStore
}
func (p *AuditPlugin[C]) Name() string {
return "audit"
}
func (p *AuditPlugin[C]) AfterTransition(ctx plugin.Context[C], from, to plugin.StateID, event plugin.Event) {
p.store.Record(AuditEntry{
Timestamp: time.Now(),
MachineID: ctx.MachineID,
From: string(from),
To: string(to),
Event: string(event.Type),
})
}
func (p *AuditPlugin[C]) OnError(ctx plugin.Context[C], err error) {
p.store.RecordError(ctx.MachineID, err)
}Plugin errors are isolated from core interpreter execution:
- Plugin panics are recovered and don't crash the interpreter
- Errors in plugins are logged but don't affect state transitions
- Use
OnErrorhook to capture and handle errors from actions
- Keep plugins focused: Each plugin should have a single responsibility
- Avoid side effects in OnEvent: Modifying events can make debugging harder
- Use composite for organization: Group related plugins together
- Handle errors gracefully: Plugins should not throw panics
- Be mindful of performance: Hooks are called synchronously