FinFocus Plugin Development
Plugin Architecture
Plugins communicate with finfocus-core via gRPC using protocol buffers from finfocus-spec.
Each plugin is a standalone binary that implements the CostSourceService.
Core CLI -> gRPC -> Plugin Binary
(CostSourceService)
Two launcher types:
- •
ProcessLauncher(TCP) - Plugin listens on a port - •
StdioLauncher(stdin/stdout) - Plugin communicates over stdio
Quick Start: New Plugin
- •Create Go module with
finfocus-specdependency - •Embed
pluginsdk.BasePluginfor common functionality - •Implement required gRPC methods
- •Build binary and install to
~/.finfocus/plugins/<name>/<version>/
import (
"github.com/rshade/finfocus-spec/sdk/go/pluginsdk"
pbc "github.com/rshade/finfocus-spec/sdk/go/proto/finfocus/v1"
)
type MyPlugin struct {
*pluginsdk.BasePlugin
logger zerolog.Logger
mu sync.Mutex
}
func (p *MyPlugin) Name() string { return "my-plugin" }
Required Methods
Every plugin must implement these CostSourceService methods:
| Method | Purpose |
|---|---|
GetProjectedCost | Estimate future costs from resource descriptors |
GetActualCost | Return historical costs for a time range |
GetRecommendations | Return cost optimization recommendations |
GetPluginInfo | Return plugin metadata (name, version, providers) |
Shutdown | Clean up resources on termination |
Request Validation
Use pluginsdk validation helpers before processing:
func (p *MyPlugin) GetProjectedCost(ctx context.Context, req *pbc.GetProjectedCostRequest) (*pbc.GetProjectedCostResponse, error) {
p.mu.Lock()
defer p.mu.Unlock()
if err := pluginsdk.ValidateProjectedCostRequest(req); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "validation: %v", err)
}
// Process request...
}
Plugin Installation
Plugins install to ~/.finfocus/plugins/<name>/<version>/:
# Build and install make build-recorder make install-recorder # -> ~/.finfocus/plugins/recorder/0.1.0/ # Verify ./bin/finfocus plugin list ./bin/finfocus plugin validate
Environment Variables
Plugins receive these from core via pluginsdk constants:
| Constant | Env Var | Purpose |
|---|---|---|
pluginsdk.EnvPort | FINFOCUS_PLUGIN_PORT | gRPC port |
pluginsdk.EnvLogLevel | FINFOCUS_LOG_LEVEL | Log verbosity |
pluginsdk.EnvLogFormat | FINFOCUS_LOG_FORMAT | json or text |
pluginsdk.EnvLogFile | FINFOCUS_LOG_FILE | Log file path |
pluginsdk.EnvTraceID | FINFOCUS_TRACE_ID | Distributed trace ID |
Trace ID Propagation
// Server-side: extract trace ID from incoming gRPC metadata
server := grpc.NewServer(
grpc.UnaryInterceptor(pluginsdk.TracingUnaryServerInterceptor()),
)
// In handlers: get trace ID from context
traceID := pluginsdk.TraceIDFromContext(ctx)
Reference Implementation
The recorder plugin at plugins/recorder/ demonstrates all patterns.
See references/recorder-reference.md for
the complete implementation walkthrough.
Plugin Communication Details
See references/pluginsdk-api.md for the full pluginsdk API, proto message types, and gRPC patterns.
Testing Plugins
go test ./plugins/recorder/... # Unit tests go test ./test/integration/recorder_test.go # Integration go test -bench=BenchmarkRecorder ./plugins/recorder/... # Perf (<10ms)
Common Pitfalls
- •Always use
sync.Mutexfor thread safety in gRPC handlers - •Return
status.Errorf(codes.InvalidArgument, ...)for validation errors - •Call
cmd.Wait()afterKill()to prevent zombie processes - •10-second timeout with 100ms retry delays for plugin startup
- •Platform-specific binary detection (Unix permissions vs Windows .exe)