Skip to content

Prevent misuse of compiled metrics#418

Merged
michaelherold merged 2 commits into
mainfrom
prevent-defining-outside-a-subclass
Apr 2, 2026
Merged

Prevent misuse of compiled metrics#418
michaelherold merged 2 commits into
mainfrom
prevent-defining-outside-a-subclass

Conversation

@michaelherold

@michaelherold michaelherold commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

✅ What

This change prevents accidental misuse of the CompiledMetric system by ensuring that you cannot define the base class, as that mutates shared, global state.

Additionally, the second commit introduces a "definition error" instead of using ArgumentError for definiton-time issues, as they aren't errors with passing invalid arguments, but with how you're setting up your definition. (Technically, this is a breaking change, but I doubt people are rescuing these ArgumentErrors, so it should be safe. I opted for a second commit so we can just not ship that if we want to avoid this "breaking change".)

🤔 Why

I saw that the API was CompiledMetric.define and assumed it worked like the Data structs. It appeared to work until I went to define a second metric, which caused things to break unexpectedly.

👩🔬 How to validate

I believe the test suite covers the behavior.

Checklist

  • I documented the changes in the CHANGELOG file.

@michaelherold michaelherold self-assigned this Mar 23, 2026

@pedro-stanaka pedro-stanaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We dont have that many downstream clients of this so far, I guess we can afford the "breaking" change.

Please add change log for this, and also decide if you want to bump version on this or in follow up PR.

Comment thread lib/statsd/instrument/compiled_metric.rb Outdated
Calling `define` directly on a base metric type class, e.g.

    MyCounter = CompiledMetric::Counter.define(name: "my_metric")

appears to work until you define another metric of the same type, where
it starts to misbehavior.

This is because definition sets class instance variables and redefines
the metric method on the base class itself, silently polluting state for
all future subclasses.

This change adds two guards to `define`:

1. A base-class guard raises `ArgumentError` when `define` is called
   on any of the base classes directly, with a message showing the
   correct subclass pattern.
2. A double-define guard raises `ArgumentError` when `define` is called
   a second time on the same class, preventing accidental state
   pollution and inconsistent behavior.
Replace `ArgumentError` with a dedicated `DefinitionError` for all
definition-time guards in `CompiledMetric`:

- Calling `define` on a base type class directly
- Calling `define` more than once on the same class
- Using a metric before `define` has been called

This makes definition errors independently rescuable and distinguishes
them from genuine argument errors (e.g. unsupported tag types, accessing
`sample_rate` before definition) and gives a consistent experience
across error types.
@michaelherold michaelherold force-pushed the prevent-defining-outside-a-subclass branch from 489e5e1 to aeb632f Compare April 2, 2026 14:31
@michaelherold

michaelherold commented Apr 2, 2026

Copy link
Copy Markdown
Contributor Author

and also decide if you want to bump version on this or in follow up PR.

It looks like the normal release pattern is to do a bump in a discrete PR so I figured that is what I would do.

@pedro-stanaka pedro-stanaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@michaelherold michaelherold merged commit 9a72b9c into main Apr 2, 2026
12 checks passed
@michaelherold michaelherold deleted the prevent-defining-outside-a-subclass branch April 2, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants