Skip to content

feat: impact metric counter#336

Merged
sjaanus merged 11 commits intomainfrom
impact-metric-counter
Dec 15, 2025
Merged

feat: impact metric counter#336
sjaanus merged 11 commits intomainfrom
impact-metric-counter

Conversation

@sjaanus
Copy link
Copy Markdown
Contributor

@sjaanus sjaanus commented Dec 15, 2025

Add first iteration of impact metrics. Counters. Next up is gauges.

Comment on lines +52 to +74
static String getLabelKey(Map<String, String> labels) {
if (labels == null || labels.isEmpty()) {
return "";
}
return labels.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(","));
}

static Map<String, String> parseLabelKey(String key) {
if (key == null || key.isEmpty()) {
return Collections.emptyMap();
}
ConcurrentHashMap<String, String> labels = new ConcurrentHashMap<>();
for (String pair : key.split(",")) {
String[] parts = pair.split("=", 2);
if (parts.length == 2) {
labels.put(parts[0], parts[1]);
}
}
return labels;
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Following same logic as we have in node SDK.

Comment on lines +45 to +47
if (samples.isEmpty()) {
samples.add(new NumericMetricSample(Collections.emptyMap(), 0L));
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Populate with default value to at least have something.

Comment on lines +3 to +7
public enum MetricType {
COUNTER,
GAUGE,
HISTOGRAM
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Preparing for gauge and histogram.

@coveralls
Copy link
Copy Markdown
Collaborator

coveralls commented Dec 15, 2025

Pull Request Test Coverage Report for Build 20232798914

Details

  • 89 of 101 (88.12%) changed or added relevant lines in 6 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.2%) to 78.768%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/main/java/io/getunleash/impactmetrics/CollectedMetric.java 16 18 88.89%
src/main/java/io/getunleash/impactmetrics/NumericMetricSample.java 10 12 83.33%
src/main/java/io/getunleash/impactmetrics/MetricOptions.java 9 17 52.94%
Totals Coverage Status
Change from base Build 20106397212: -0.2%
Covered Lines: 2646
Relevant Lines: 3252

💛 - Coveralls

private final String name;
private final String help;
private final MetricType type;
private final List<NumericMetricSample> samples;
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.

this will nor work for histograms

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.

they have BucketMetricSamples

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes histograms is different, but I will solve it when I get there.

if (key == null || key.isEmpty()) {
return Collections.emptyMap();
}
ConcurrentHashMap<String, String> labels = new ConcurrentHashMap<>();
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.

you're not sharing this so Map<String, String> labels = new HashMap<>(); would suffice.

(key, value) -> {
samples.add(new NumericMetricSample(parseLabelKey(key), value));
});
values.clear();
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.

increments between forEach and clear can be lost. Java is not running single threaded event loop with run to completion guarantee as Node

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

good point, fixing.


CollectedMetric result = counter.collect();

assertThat(result.getName()).isEqualTo("test_counter");
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.

looks like missing equals implementation on a result


@Test
public void should_increment_by_default_value() {
CounterImpl counter = new CounterImpl("test_counter", "test help");
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.

it doesn't follow testing convention from node where we have registry, counter on a registry and registry verification

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we do not have registry yet.

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.

the test is showing behavior we don't want to expose as public. I think Node deliberately does not export CounterImpl


assertThat(result.getSamples()).hasSize(3);

NumericMetricSample sampleAX =
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.

this logic it too complex for unit tests that should be declarative. we should strive for a single assertion as in node. prbbaly another missing equals implementation leaking complexity to the unit test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The point was to check that things do not get bucketed in wrong places and provide real world example, with different buckets.

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.

Sure, I agree with the intent. My point is that we're still missing equals and hashCode on NumericMetricSample and CollectedMetric that would make some of the checks easier to parse

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.

what we do as toMatchObject in Node for value object equality checks is typically implemented in equals methods in Java and tests don't need to dissect every single field in a separate assertion

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.

e.g.

        assertThat(result.getSamples())
                .containsExactlyInAnyOrder(
                        sample(Map.of("a", "x"), 1L), sample(Map.of("b", "y"), 2L), sample(3L))

public void should_parse_label_key_correctly() {
String key = "env=prod,region=us-east";

Map<String, String> labels = CounterImpl.parseLabelKey(key);
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.

this is private bahavior that should not be exposed and only tested transitively as part of the core behavior

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.

Node doesn't export parseLabelKey

}

@Test
public void should_increment_with_custom_value() {
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.

in Node we had one test for values and labels. keeping test structure similar make it easier to review them side by side

Map<String, String> labels1 = Map.of("z", "1", "a", "2");
Map<String, String> labels2 = Map.of("a", "2", "z", "1");

String key1 = CounterImpl.getLabelKey(labels1);
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.

same as the next comment

import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class CounterImpl implements Counter {
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.

if we want to keep it compliant with Node then it should not be public

@github-project-automation github-project-automation Bot moved this from New to Approved PRs in Issues and PRs Dec 15, 2025
@sjaanus sjaanus merged commit c256b87 into main Dec 15, 2025
9 checks passed
@sjaanus sjaanus deleted the impact-metric-counter branch December 15, 2025 13:02
@github-project-automation github-project-automation Bot moved this from Approved PRs to Done in Issues and PRs Dec 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants