README
¶
Flutter

Flutter takes a script in YAML and generates metrics and other telemetry to simulate various test scenarios.
A metric generator is defined that has a start time and various configuration options (aka "spec") and metrics can refer to these generators to form telemetry. Generators can be altered at different points in time. Metrics can use one or more generator to form the datapoint values.
Installation
You can run it without insalling:
go run github.com/cardinalhq/flutter/cmd/flutter@latest confg.yaml [other.yaml ...]
Or you can install it as a command:
go install github.com/cardinalhq/flutter/cmd/flutter@latest
Usage
The options are one or more configuration files in YAML format. They are merged while loading, with the later options (usually) overwriting the earlier ones.
Example
% go run github.com/cardinalhq/flutter/cmd/flutter@latest sample-dryrun.yaml sample-config.yaml
2025/04/30 10:44:12 INFO MetricGauge Emit ts=10s metricName=pod.cpu.usage value=8.603450562309138
2025/04/30 10:44:12 INFO MetricGauge Emit ts=20s metricName=pod.cpu.usage value=8.9923406286904
2025/04/30 10:44:12 INFO MetricGauge Emit ts=30s metricName=pod.cpu.usage value=9.15036693509603
2025/04/30 10:44:12 INFO MetricGauge Emit ts=40s metricName=pod.cpu.usage value=7.507197702329614
2025/04/30 10:44:12 INFO MetricGauge Emit ts=50s metricName=pod.cpu.usage value=9.966095793585012
2025/04/30 10:44:12 INFO MetricGauge Emit ts=1m0s metricName=pod.cpu.usage value=61.27401607743047
2025/04/30 10:44:12 INFO MetricGauge Emit ts=1m10s metricName=pod.cpu.usage value=62.90178020482533
2025/04/30 10:44:12 INFO MetricGauge Emit ts=1m20s metricName=pod.cpu.usage value=58.31397815651833
2025/04/30 10:44:12 INFO MetricGauge Emit ts=1m30s metricName=pod.cpu.usage value=61.26050101150872
2025/04/30 10:44:12 INFO MetricGauge Emit ts=1m40s metricName=pod.cpu.usage value=60.841981633174264
2025/04/30 10:44:12 INFO MetricGauge Emit ts=1m50s metricName=pod.cpu.usage value=58.18683152763096
2025/04/30 10:44:12 INFO MetricGauge Emit ts=2m0s metricName=pod.cpu.usage value=60.589915194816385
Configuration
This and other samples are in the various sample-*.yaml files.
duration: 120s
seed: 123456789
#wallclockStart: 2000-01-01T00:00:00Z
script:
- type: metricGenerator
name: constant10
spec:
type: constant
value: 10
- type: metricGenerator
name: random5
spec:
type: randomWalk
target: 0
stepSize: 4
elasticity: 0.5
variation: 10
- type: metric
name: pod.cpu.usage
spec:
attributes:
resource:
k8s.cluster.name: fakecluster
k8s.namespace.name: fakenamespace
k8s.pod.name: fakepod
type: gauge
frequency: 10s
generators:
- constant10
- random5
- type: metricGenerator
at: 60s
name: constant10
spec:
type: constant
value: 60
Run State
durationis optional, and will be computed from the last scriptatvalue plus one second.seedis optional, but recommended to produce repeatable scripts. If it is not set, the current time is used as a seed, resulting in different output each run for components that use randomness.otlpDestinationdefines where to produced telemetry.wallclockStartis optional. If unset, the current time is used. Otherwise, the script will simulate starting at this time.dryrunindicates that the script should run as fast as possible and produce no metric output.
Script
The script defines what happens, and when. Each script element has several common fields:
atis when this component state is applied.typeindicates the type of component.nameis the name of the component. Various components use the name differently.specis the component definition.
Component Types
metricGeneratordefines a component that produces a floating point value. These are typically defined once at time0and then modified later to change based on the metric output wanted.metricdefines a metric that is emitted on a timer, based on its spec. Metrics use a series ofmetricGeneratorsto build their values.
Generators
Constant
Constant generators always produce the same value. When redefined, they will jump directly to the new value.
spec:
type: constant
value: 60
Ramp
Ramp generators use linear intrepolation from start to target over the time duration.Once they reach their target, that value is emitted for all future samples. Prior to the start time, the start value is emitted.
When a ramp is reconfigured, the current value is used as the new starting value, and the new rule takes over immediately. For example, if a ramp is from 10 to 100 over 10m, and at 5m it is changed, the interpolated value will be used as a start for the new ramp shape.

duration: 10m
seed: 123456789
script:
- type: metricGenerator
name: ramp
spec:
type: ramp
target: 1.0
duration: 2m
- type: metric
name: pod.cpu.usage
spec:
attributes:
resource:
k8s.cluster.name: fakecluster
k8s.namespace.name: fakenamespace
k8s.pod.name: fakepod
type: gauge
frequency: 10s
generators:
- ramp
- type: metricGenerator
at: 2m
name: ramp
spec:
type: ramp
target: 0.75
duration: 8m
- type: metricGenerator
at: 3m
name: ramp
spec:
type: ramp
target: 0.01
duration: 3m
Random Walk
Random Walk emits a mean-reverting noise term. For each sample x, the internal state steps like:
x ← x + elasticity*(target - x) + Uniform(−stepSize,+stepSize)
It is then clamped into [target−variation, target+variation].
If a constant is desired, variation can be set to 0, which will clamp to the target. Random samples are still generated, but the output will be constant.
spec:
type: randomWalk
target: 0
elasticity: 0.2
stepSize: 2
variation: 10
GaussianNoise
MetricGaussianNoise emits independent normal noise centered on Target. On each Emit(), it samples:
x ~ Normal(Target, StdDev²)
It is then clampd into [Target-Variation, Target+Variation].
spec:
type: gaussianNoise
target: 0
variation: 8
stdDev: 2.66
direction: both
targetis the mean for two-sided, or the lower or upper bounds for positive and negative sided.variationis the maximum range used for scaling and clamping.stdDevsets the standard deviation. If left unspecified, it is set tovariaion/3.directionspecifies if we should have positive and negative, or just postitive or just negative random values.
Spiky Noise
spikyNoise configures a mostly‐zero generator that randomly spikes with Poisson‐distributed counts when “ON”.

spec:
type: spikyNoise
pStart: 0.05
pEnd: 0.2
peakTarget: 15
variation: 5
Exporters
Metric
A metric exporter emits values at some defined rate, using one or more generators to define each emitted sample. The component at indicates when the metric should fist appear. It is common to omit at, causing the metric to always be present. The component name will be used as the metric name by default, but it can also be included in the spec, allowing multiple metrics with the same name but different attributes.
Metrics have these common fields:
attributes.resource,attributes.scope, andattributes.datapointdefine the OpenTelemetry metric resource, scope, and datapoint attributes for each sample emitted.generatorsis a list of metric generators that will be used to calculate each sample. At least one is required.frequencyis the rate at which this metric will be produced. Defaults to10s.typesets the type, such asgaugeorcounter. Types may include additional fields.namesets the metric name used during export. This defaults to the componet name if not set.
Producing Metric Output
The top-level otlpDestination defines how to send OTLP-format telemetry. This is
usually an OpenTelemetry otlp http receiver.
Fields:
endpointdefines the base endpoint, usually without a path, such ashttps://example.com:1234headersdefines amap[string]stringof headers to send with each HTTP request.timeoutsets the maximum wait time for the post to complete. Defaults to5s.
Future Work
- Add a way to more carefully tune the sampler pipeline, with clamping, simple math, etc. This would probably be inside the