Developer Guide
This guide is for engineers and developers who want to extend FinFocus by building plugins or contributing to the core project.
Table of Contents
Section titled “Table of Contents”- Getting Started
- Architecture Overview
- Development Setup
- Building Plugins
- Contributing to Core
- Testing
- Deployment
Getting Started
Section titled “Getting Started”Prerequisites
Section titled “Prerequisites”- Go 1.25.8+ (for core development)
- Git
- Make
- Node.js 18+ (for documentation tools)
- Docker (optional, for containerized testing)
Quick Setup
Section titled “Quick Setup”# Clone repositorygit clone https://github.com/rshade/finfocuscd finfocus
# Buildmake build
# Testmake test
# Run./bin/finfocus --helpArchitecture Overview
Section titled “Architecture Overview”Core Components
Section titled “Core Components”┌─────────────────┐│ Pulumi JSON │ User's infrastructure definition└────────┬────────┘ │ ▼┌─────────────────┐│ Ingestion │ Parse resources from Pulumi output└────────┬────────┘ │ ▼┌─────────────────┐│ Engine │ Orchestrate cost calculation├─────────────────┤│ • Resource ││ Mapping ││ • Cost ││ Calculation ││ • Aggregation │└────────┬────────┘ │ ┌────┴─────┐ ▼ ▼┌────────┐ ┌──────┐│Plugins │ │Specs │ Cost sources└────────┘ └──────┘ │ │ └────┬─────┘ │ ▼┌─────────────────┐│ Output │ Table, JSON, NDJSON└─────────────────┘Key Packages
Section titled “Key Packages”| Package | Purpose |
|---|---|
internal/cli | Command-line interface (Cobra) |
internal/engine | Core cost calculation logic |
internal/ingest | Pulumi plan parsing |
internal/pluginhost | Plugin gRPC communication |
internal/registry | Plugin discovery |
internal/spec | Local pricing specifications |
internal/analyzer | Pulumi Analyzer gRPC server |
pkg/pluginsdk | Plugin SDK for developers |
Pulumi Analyzer Integration (Developer Perspective)
Section titled “Pulumi Analyzer Integration (Developer Perspective)”The internal/analyzer package implements the Pulumi Analyzer gRPC protocol, allowing
FinFocus to act as a “zero-click” cost analysis tool during pulumi preview.
Developers extending or debugging the Analyzer should be aware of:
- gRPC Protocol: Communication between Pulumi CLI and analyzer occurs via gRPC.
- Port Handshake: The analyzer server communicates its dynamic port to the Pulumi CLI via stdout. All other logging goes to stderr.
- Resource Mapping: The analyzer converts Pulumi resource structures
(
pulumirpc.AnalyzerResource) intoengine.ResourceDescriptorfor cost calculation. - Diagnostics: Cost estimates are returned as
ADVISORYdiagnostics.
For a detailed architectural overview of the Analyzer, refer to the Analyzer Architecture documentation.
Development Setup
Section titled “Development Setup”Local Development
Section titled “Local Development”# Install dependenciesgo mod download
# Build binarymake build
# Run with example plan./bin/finfocus cost projected \ --pulumi-json examples/plans/aws-simple-plan.json
# Run testsmake test
# Run lintersmake lintDocumentation Development
Section titled “Documentation Development”# Install Ruby dependenciescd docsbundle installcd ..
# Serve docs locallymake docs-serve# Visit http://localhost:4000/finfocus/
# Lint docsmake docs-lintIDE Setup
Section titled “IDE Setup”VS Code:
{ "go.lintOnSave": "package", "go.useLanguageServer": true, "[go]": { "editor.formatOnSave": true, "editor.defaultFormatter": "golang.go" }}Building Plugins
Section titled “Building Plugins”Quick Plugin Template
Section titled “Quick Plugin Template”Create a new plugin project:
cd ../mkdir finfocus-plugin-myservicecd finfocus-plugin-myservicego mod init github.com/yourname/finfocus-plugin-myserviceMinimal Plugin
Section titled “Minimal Plugin”package main
import ( "context" "log"
pb "github.com/rshade/finfocus-spec/sdk/go/proto/finfocus/v1")
type MyPlugin struct{}
func (p *MyPlugin) GetProjectedCost(ctx context.Context, req *pb.GetProjectedCostRequest) (*pb.GetProjectedCostResponse, error) {
// Fetch cost from your API costs := make([]*pb.Cost, 0)
for _, resource := range req.Resources { cost := &pb.Cost{ ResourceId: resource.Id, TotalCost: calculateCost(resource), Currency: "USD", } costs = append(costs, cost) }
return &pb.GetProjectedCostResponse{ Costs: costs, }, nil}
func calculateCost(resource *pb.Resource) float64 { // Your cost calculation logic return 0.0}Full Plugin Development
Section titled “Full Plugin Development”See Plugin Development Guide for:
- Complete implementation walkthrough
- gRPC service setup
- Error handling patterns
- Testing strategies
- Deployment instructions
Example: Vantage Plugin
Section titled “Example: Vantage Plugin”The Vantage plugin is a complete reference implementation:
# See implementation atcat ../finfocus-plugin-vantage/main.goContributing to Core
Section titled “Contributing to Core”Setting Up Dev Branch
Section titled “Setting Up Dev Branch”# Fetch latestgit fetch upstream
# Create feature branchgit checkout -b feature/my-feature upstream/main
# Make changes# ... edit files ...
# Test changesmake testmake lintmake docs-validateCode Style
Section titled “Code Style”Go:
- Follow Effective Go
- Run
gofmton your code - Use
golangci-lintfor linting - Write clear variable names
- Add godoc comments for exported functions
Example:
// GetActualCost retrieves actual historical costs for resources.// It supports filtering by tags and grouping by dimension.func (e *Engine) GetActualCost(ctx context.Context, req *ActualCostRequest) (*ActualCostResponse, error) { // Implementation}Markdown:
- Follow Google style guide
- Use clear headings
- Provide code examples
- Run
make docs-lintbefore committing
Logging Patterns
Section titled “Logging Patterns”FinFocus uses zerolog for structured logging with distributed tracing. Follow these patterns:
Getting a Logger:
// From context (preferred - includes trace ID)log := logging.FromContext(ctx)log.Debug(). Ctx(ctx). Str("component", "engine"). Str("operation", "get_projected_cost"). Int("resource_count", len(resources)). Msg("starting projected cost calculation")Component Loggers:
Each package should identify itself with a component field:
// In CLI packagelogger = logging.ComponentLogger(logger, "cli")
// Or inline for context-based logginglog.Info(). Ctx(ctx). Str("component", "engine"). Msg("operation complete")Standard Log Fields:
| Field | Purpose | Example |
|---|---|---|
component | Package identifier | ”cli”, “engine”, “registry” |
operation | Current operation | ”get_projected_cost”, “load_plan” |
trace_id | Request correlation (auto-injected) | “01HQ7X2J3K4M5N6P7Q8R9S0T1U” |
duration_ms | Operation timing | Dur("duration_ms", elapsed) |
Logging Levels:
// Trace - Very detailed debugginglog.Trace().Ctx(ctx).Str("component", "engine").Msg("internal detail")
// Debug - Detailed troubleshooting infolog.Debug().Ctx(ctx).Str("component", "engine").Msg("querying plugin")
// Info - Normal operationslog.Info().Ctx(ctx).Str("component", "engine").Msg("calculation complete")
// Warn - Something unexpected but recoverablelog.Warn().Ctx(ctx).Str("component", "engine").Err(err).Msg("plugin timeout, using fallback")
// Error - Something failedlog.Error().Ctx(ctx).Str("component", "engine").Err(err).Msg("calculation failed")Sensitive Data Protection:
// Use SafeStr for potentially sensitive key-value pairslogging.SafeStr(event, "api_key", apiKey) // Automatically redacts sensitive keysTrace ID Management:
// Generate trace ID at entry pointtraceID := logging.GetOrGenerateTraceID(ctx)ctx = logging.ContextWithTraceID(ctx, traceID)
// TracingHook automatically injects trace_id into all log entries// when using .Ctx(ctx)Commit Messages
Section titled “Commit Messages”type: Brief description
More detailed explanation of changes.- What changed- Why it changed- Any implementation notes
Closes #123Types:
feature- New functionalityfix- Bug fixesdocs- Documentationtest- Testsrefactor- Code restructuringperf- Performance improvements
Testing Your Changes
Section titled “Testing Your Changes”# Run all testsmake test
# Run specific packagego test -v ./internal/engine/...
# Run with coveragego test -cover ./...
# Run specific testgo test -run TestActualCost ./internal/engine/...Pull Request Process
Section titled “Pull Request Process”-
Update from main:
Terminal window git fetch upstreamgit rebase upstream/main -
Run all checks:
Terminal window make testmake lintmake validatemake docs-validate -
Push and create PR:
Terminal window git push origin feature/my-feature# Create PR on GitHub -
Address feedback:
- Respond to comments
- Push additional commits
- Rebase if requested
Testing
Section titled “Testing”Unit Tests
Section titled “Unit Tests”# Run all testsmake test
# Run with coveragego test -cover ./...
# View coverage reportgo test -coverprofile=coverage.out ./...go tool cover -html=coverage.outTest Structure
Section titled “Test Structure”func TestGetActualCost(t *testing.T) { // Arrange - Set up test data request := &ActualCostRequest{ StartDate: "2024-01-01", EndDate: "2024-01-31", }
// Act - Execute function response, err := engine.GetActualCost(context.Background(), request)
// Assert - Verify results if err != nil { t.Fatalf("unexpected error: %v", err) } if response == nil { t.Fatal("expected response, got nil") }}Integration Testing
Section titled “Integration Testing”For testing with real plugins:
# Ensure plugins are installed./bin/finfocus plugin list
# Test with example plan./bin/finfocus cost projected --pulumi-json examples/plans/aws-simple-plan.jsonPlugin Certification
Section titled “Plugin Certification”Before releasing a plugin, run the certification suite to ensure full protocol compliance:
finfocus plugin certify ./path/to/your-pluginAnalyzer Integration Testing
Section titled “Analyzer Integration Testing”Testing the Analyzer involves running pulumi preview against a Pulumi project
configured to use the finfocus analyzer serve command.
# Example: Configure your Pulumi.yaml as described in the Analyzer Setup guide.# Then, navigate to your Pulumi project directory:cd your-pulumi-projectpulumi previewVerify the output for cost diagnostics. For detailed debugging, enable verbose logging:
FINFOCUS_LOG_LEVEL=debug pulumi previewCross-Provider Aggregation Testing
Section titled “Cross-Provider Aggregation Testing”Test cross-provider aggregation by running finfocus cost actual with --group-by daily
or --group-by monthly on a Pulumi plan that includes resources from multiple providers.
# Example: Daily aggregationfinfocus cost actual --pulumi-json examples/plans/multi-provider-plan.json \ --from 2024-01-01 --to 2024-01-31 --group-by daily
# Example: Monthly aggregation with JSON outputfinfocus cost actual --pulumi-json examples/plans/multi-provider-plan.json \ --from 2024-01-01 --group-by monthly --output jsonFuzz Testing
Section titled “Fuzz Testing”FinFocus uses Go’s native fuzzing (Go 1.25+) for parser resilience testing:
# JSON parser fuzzinggo test -fuzz=FuzzJSON$ -fuzztime=30s ./internal/ingest
# YAML parser fuzzinggo test -fuzz=FuzzYAML$ -fuzztime=30s ./internal/spec
# Full plan parsing fuzzinggo test -fuzz=FuzzPulumiPlanParse$ -fuzztime=30s ./internal/ingestFuzz test files:
| Location | Purpose |
|---|---|
internal/ingest/fuzz_test.go | JSON parser fuzz tests |
internal/spec/fuzz_test.go | YAML spec fuzz tests |
Adding seed corpus:
Place interesting inputs in testdata/fuzz/<TestName>/ directories:
internal/ingest/testdata/fuzz/FuzzJSON/├── valid_plan.json├── edge_case_unicode.json└── malformed_input.jsonPerformance Benchmarks
Section titled “Performance Benchmarks”Benchmarks test scalability with synthetic data:
# Run all benchmarksgo test -bench=. -benchmem ./test/benchmarks/...
# Run scale benchmarks onlygo test -bench=BenchmarkScale -benchmem ./test/benchmarks/...
# Run with specific iterationsgo test -bench=BenchmarkScale1K -benchtime=10x -benchmem ./test/benchmarks/...Benchmark test files:
| Location | Purpose |
|---|---|
test/benchmarks/scale_test.go | Scale tests (1K-100K) |
test/benchmarks/generator/ | Synthetic data generator |
Performance targets:
| Scale | Target Time | Actual (baseline) |
|---|---|---|
| 1K | < 1 second | ~13ms |
| 10K | < 30 seconds | ~167ms |
| 100K | < 5 minutes | ~2.3s |
Synthetic Data Generator
Section titled “Synthetic Data Generator”The benchmark generator creates realistic infrastructure plans:
import "github.com/rshade/finfocus/test/benchmarks/generator"
// Use preset configurationsplan, err := generator.GeneratePlan(generator.PresetSmall) // 1K resourcesplan, err := generator.GeneratePlan(generator.PresetMedium) // 10K resourcesplan, err := generator.GeneratePlan(generator.PresetLarge) // 100K resources
// Custom configurationconfig := generator.BenchmarkConfig{ ResourceCount: 5000, MaxDepth: 5, DependencyRatio: 0.3, Seed: 42, // Deterministic generation}plan, err := generator.GeneratePlan(config)Generator features:
- Deterministic output with seed values
- Configurable resource count and nesting depth
- Realistic resource types (AWS, Azure, GCP)
- Dependency graph generation
- JSON export for external tooling
Deployment
Section titled “Deployment”Building Releases
Section titled “Building Releases”# Create version taggit tag v0.1.0git push origin v0.1.0
# GitHub Actions automatically:# 1. Builds cross-platform binaries# 2. Creates release# 3. Uploads checksumsPlugin Installation
Section titled “Plugin Installation”Users install plugins to: ~/.finfocus/plugins/<name>/<version>/
Structure:
~/.finfocus/plugins/├── myplugin/│ └── 0.1.0/│ ├── finfocus-myplugin # Plugin binary│ └── plugin.manifest.json # MetadataDocker Deployment
Section titled “Docker Deployment”FROM golang:1.25.8 as builderWORKDIR /appCOPY . .RUN make build
FROM alpine:latestCOPY --from=builder /app/bin/finfocus /usr/local/bin/ENTRYPOINT ["finfocus"]Useful Commands
Section titled “Useful Commands”# Developmentmake build # Build binarymake test # Run testsmake lint # Code lintingmake validate # Validationmake clean # Clean artifacts
# Documentationmake docs-lint # Lint docsmake docs-serve # Serve locallymake docs-build # Build sitemake docs-validate # Validate structure
# Gitgit fetch upstream # Get latest changesgit rebase upstream/main # Rebase on maingit push origin branch # Push changesResources
Section titled “Resources”- Plugin Development: Plugin Development Guide
- Plugin SDK: Plugin SDK Reference
- Examples: Code Examples
- Architecture: System Architecture
- Contributing: Contributing Guide
- Vantage Plugin: Vantage Implementation Example
Last Updated: 2025-10-29