Analyzer Architecture
FinFocus integrates with the Pulumi engine via the Pulumi Analyzer gRPC protocol,
enabling zero-click cost estimation during pulumi preview and pulumi up. The
analyzer intercepts resource definitions in real time, calculates projected costs
through the normal plugin pipeline, and returns advisory diagnostics that Pulumi
displays alongside its diff output.
Overview
Section titled “Overview”The analyzer runs as a gRPC server that Pulumi launches as a subprocess. Pulumi sends individual resource definitions as they are evaluated, then sends the complete stack state once evaluation finishes. FinFocus returns cost diagnostics at both levels. Enforcement is always ADVISORY: diagnostics are informational and never block a deployment.
The implementation lives in internal/analyzer/ and is exposed through the
finfocus analyzer serve CLI command (internal/cli/analyzer_serve.go).
Protocol
Section titled “Protocol”FinFocus implements the pulumirpc.AnalyzerServer interface from the Pulumi SDK
(github.com/pulumi/pulumi/sdk/v3/proto/go). The server registers the following
RPCs.
| RPC | Purpose |
|---|---|
Handshake |
Completes the Pulumi analyzer handshake |
ConfigureStack |
Receives stack and project context; clears per-resource cost cache |
Analyze |
Receives a single resource; calculates and caches its projected cost |
AnalyzeStack |
Receives all resources; aggregates cached costs and emits summary diagnostics |
GetAnalyzerInfo |
Returns analyzer identity and active policy list |
GetPluginInfo |
Returns plugin version metadata |
Cancel |
Accepts a cancellation request (no-op; Pulumi always continues) |
Note: Analyze is the per-resource RPC. AnalyzeStack fires once at the end of
evaluation with the complete stack. Both are required by the protocol.
Port Handshake
Section titled “Port Handshake”The Pulumi Analyzer protocol requires a specific startup sequence.
finfocus analyzer servebinds a gRPC listener on a random TCP port.- It prints only the port number to stdout, for example
52341. - All log output goes to stderr exclusively.
- Pulumi reads the port from stdout and connects.
This constraint means the analyzer command must not write anything else to stdout before or after printing the port, including log lines.
Interaction Sequence
Section titled “Interaction Sequence”sequenceDiagram participant P as Pulumi Engine participant A as finfocus analyzer serve participant E as Engine (cost pipeline) participant PL as Plugins (gRPC)
P->>A: launch subprocess A-->>P: print port to stdout P->>A: Handshake() A-->>P: ack
P->>A: ConfigureStack(stack, project) A->>A: clear per-resource cost cache A-->>P: ack
loop for each resource in preview P->>A: Analyze(resource) A->>E: GetProjectedCost(ResourceDescriptor) E->>PL: gRPC GetProjectedCost PL-->>E: CostResponse E-->>A: CostResult A->>A: store result in per-resource cache A-->>P: []AnalyzeDiagnostic (per-resource cost) end
P->>A: AnalyzeStack(allResources) A->>A: BuildCostSummary(cached results) A->>A: WriteCostSummary → last-cost-summary.json (atomic) A-->>P: []AnalyzeDiagnostic (stack summary + threshold)
P->>A: Cancel() [optional] A-->>P: ackKey Components
Section titled “Key Components”Server
Section titled “Server”Server in internal/analyzer/ implements pulumirpc.AnalyzerServer. It holds
a reference to the FinFocus engine, a zerolog logger, and an in-memory map used
to accumulate per-resource cost results between Analyze calls and the final
AnalyzeStack call.
Resource Mapping
Section titled “Resource Mapping”MapResource(res *pulumirpc.AnalyzerResource) engine.ResourceDescriptor converts
a Pulumi resource representation into the internal format the engine accepts.
MapResources applies the same transformation to a slice. The mapping extracts:
Providerfrom the Pulumi type token prefix (e.g.,awsfromaws:ec2/instance:Instance)ResourceTypeas the full Pulumi type tokenSKUfrom resource inputs (instanceType,type, etc.)Regionfrom resource inputs (availabilityZone,region)
Diagnostic Constructors
Section titled “Diagnostic Constructors”| Function | Output |
|---|---|
CostToDiagnostic(result) |
Per-resource AnalyzeDiagnostic with projected monthly cost |
StackSummaryDiagnostic(summary) |
Aggregate stack-level cost summary diagnostic |
WarningDiagnostic(message) |
MEDIUM-severity warning for error conditions |
All diagnostics carry EnforcementLevel = ADVISORY, which means Pulumi displays
them but does not fail the operation.
Cost Aggregation
Section titled “Cost Aggregation”BuildCostSummary(results []CostResult) CostSummary aggregates the per-resource
results accumulated during Analyze calls. It filters out failed results, detects
mixed currencies, and produces a total. The summary is used by both
StackSummaryDiagnostic and the threshold check.
WriteCostSummary(path string, summary CostSummary) error serializes the summary
to last-cost-summary.json using an atomic write pattern (write to a temp file,
then rename) to prevent partial reads.
The file is written under the Pulumi project’s .finfocus/ directory. Pulumi
before 3.270.0 starts the analyzer in the project, so the analyzer finds the
project itself. From 3.270.0 the analyzer runs in the policy pack directory, and
the language host passes the project (RunPluginRequest.pwd) as
FINFOCUS_PROJECT_DIR. Project-tier config and cost history use the same
directory. An explicit FINFOCUS_PROJECT_DIR in the environment takes
precedence.
Per-Resource Cache Lifecycle
Section titled “Per-Resource Cache Lifecycle”The analyzer maintains an in-memory map of resource URN to CostResult.
ConfigureStackclears the map, ensuring a fresh accumulation for each run.Analyzepopulates the map after calling the engine.AnalyzeStackreads the map, aggregates, and clears it.
This avoids re-querying plugins during AnalyzeStack for resources already priced
during Analyze.
Cost Threshold Enforcement (v0.3.0)
Section titled “Cost Threshold Enforcement (v0.3.0)”When config.Analyzer.MaxMonthlyCost is greater than zero, AnalyzeStack emits
an additional threshold diagnostic after the stack summary.
| Condition | Severity | Meaning |
|---|---|---|
| Total cost <= threshold | MEDIUM | Stack is within the configured budget |
| Total cost > threshold | HIGH | Stack exceeds the configured budget |
Both severities use EnforcementLevel = ADVISORY. The threshold diagnostic is
skipped when:
- The result set contains mixed currencies.
- All resources failed cost calculation (no valid total is available).
The threshold is configured via config.yaml:
analyzer: max_monthly_cost: 500.00Or via environment variable:
export FINFOCUS_MAX_MONTHLY_COST=500.00Policy List
Section titled “Policy List”GetAnalyzerInfo returns the analyzer identity and its active policies. The policy
list varies based on configuration.
| Policy Name | Always Active | Condition |
|---|---|---|
cost-estimate |
Yes | Always returned |
stack-cost-summary |
Yes | Always returned |
cost-threshold |
No | Only when MaxMonthlyCost > 0 |
Graceful Shutdown
Section titled “Graceful Shutdown”The server registers signal handlers for SIGINT and SIGTERM. On receipt, it
closes cost plugin processes, including their process group on Linux and macOS,
and then stops the gRPC server. It does not wait for Pulumi to hang up first:
Pulumi waits for the analyzer process to exit, so that wait would never reach
plugin cleanup. Pulumi treats a lost analyzer connection as non-fatal when the
analyzer never served.
CLI Usage
Section titled “CLI Usage”# Start the analyzer server (Pulumi invokes this automatically)finfocus analyzer serve
# Enable debug logging to stderrfinfocus analyzer serve --debugInvoke the analyzer via the --policy-pack flag. The binary must be on your PATH:
export PATH="${HOME}/.finfocus/analyzer:${PATH}"pulumi preview --policy-pack ~/.finfocus/analyzerPulumi 3.270.0 and later finds pulumi-language-finfocus on PATH and starts
it as the language host. RunPlugin starts the analyzer in the policy pack
directory, reads the port from its output, and connects over gRPC for the
preview or update. Pulumi before 3.270.0 still starts
pulumi-analyzer-policy-finfocus from PATH and reads the port the same way.
Note: Adding
analyzers:toPulumi.yamldoes not work for YAML-runtime projects. Use--policy-packwith the binary on PATH.
Logging
Section titled “Logging”All log output uses zerolog at the component level "analyzer". Logging goes
exclusively to stderr. The port number printed to stdout must not be prefixed or
suffixed with any other text.
Related Documentation
Section titled “Related Documentation”- System Overview - High-level architecture
- Cost Calculation - Engine calculation details
- Plugin Protocol - Plugin gRPC protocol
- CLI Reference - All CLI commands and flags