Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,38 @@ void testDoubleHistogram() {
points.get("test:double-histogram" + WITH_ATTRS));
}

@Test
void testLongHistogramOverflow() {
io.opentelemetry.api.metrics.LongHistogram histogram =
meter.histogramBuilder("long-histogram-overflow").ofLongs().build();
histogram.record(20_000); // exceeds highest default boundary of 10_000
OtelMetricRegistry.INSTANCE.collectMetrics(meterReader);

assertEquals(
new HistogramData(
1.0,
Arrays.asList(10_000.0, Double.POSITIVE_INFINITY),
Arrays.asList(0.0, 1.0),
20_000.0),
points.get("test:long-histogram-overflow"));
}

@Test
void testDoubleHistogramOverflow() {
io.opentelemetry.api.metrics.DoubleHistogram histogram =
meter.histogramBuilder("double-histogram-overflow").build();
histogram.record(20_000.5); // exceeds highest default boundary of 10_000
OtelMetricRegistry.INSTANCE.collectMetrics(meterReader);

assertEquals(
new HistogramData(
1.0,
Arrays.asList(10_000.0, Double.POSITIVE_INFINITY),
Arrays.asList(0.0, 1.0),
20_000.5),
points.get("test:double-histogram-overflow"));
}

@Test
void testObservableLongCounter() {
AutoCloseable observable =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,24 @@ public static byte[] recordDataPointMessage(GrowableBuffer buf, OtlpDataPoint po
writeI64(buf, histogram.min);
writeTag(buf, 12, I64_WIRE_TYPE);
writeI64(buf, histogram.max);
for (double bucketCount : histogram.bucketCounts) {
writeTag(buf, 6, I64_WIRE_TYPE);
writeI64(buf, (long) bucketCount);
}
for (double bucketBoundary : histogram.bucketBoundaries) {
writeTag(buf, 7, I64_WIRE_TYPE);
writeI64(buf, bucketBoundary);
if (!histogram.bucketCounts.isEmpty()) {
boolean hasOverflow = false;
for (double bucketBoundary : histogram.bucketBoundaries) {
if (!Double.isInfinite(bucketBoundary)) {
writeTag(buf, 7, I64_WIRE_TYPE);
writeI64(buf, bucketBoundary);
} else {
hasOverflow = true; // don't write the overflow boundary
}
}
for (double bucketCount : histogram.bucketCounts) {
writeTag(buf, 6, I64_WIRE_TYPE);
writeI64(buf, (long) bucketCount);
}
if (!hasOverflow) { // write one more count than boundaries
writeTag(buf, 6, I64_WIRE_TYPE);
writeI64(buf, 0L);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,63 +357,92 @@ static Stream<Arguments> cases() {
boolAttr("success", true),
dblAttr("rate", 0.5))))),

// ── histogram — no buckets ────────────────────────────────────────────
// ── histogram — overflow only (no explicit bounds) ────────────────────
Arguments.of(
"histogram no buckets",
"histogram no explicit bounds — overflow bucket only",
asList(
scope(
"io.hist",
histogram("response.time", 1.0, emptyList(), asList(1.0), 0.5, 0.5, 0.5)))),
histogram(
"response.time",
1.0,
asList(Double.POSITIVE_INFINITY),
asList(1.0),
0.5,
0.5,
0.5)))),

// ── histogram — zero count and sum ────────────────────────────────────
// ── histogram — zero count and sum with overflow bucket ───────────────
Arguments.of(
"histogram zero count and sum",
asList(
scope(
"io.hist",
histogram("idle.time", 0.0, emptyList(), asList(0.0), 0.0, 0.0, 0.0)))),

// ── histogram — single explicit bound ─────────────────────────────────
histogram(
"idle.time",
0.0,
asList(Double.POSITIVE_INFINITY),
asList(0.0),
0.0,
0.0,
0.0)))),

// ── histogram — single explicit bound with overflow ───────────────────
Arguments.of(
"histogram single bound",
"histogram single bound with overflow",
asList(
scope(
"io.hist",
histogram(
"request.size",
5.0,
asList(100.0),
asList(100.0, Double.POSITIVE_INFINITY),
asList(4.0, 1.0),
280.0,
20.0,
200.0)))),

// ── histogram — with explicit bounds and attrs ────────────────────────
// ── histogram — finite bounds only (no overflow) — extra zero appended ─
Arguments.of(
"histogram finite bounds — no overflow — extra zero bucket appended",
asList(
scope(
"io.hist",
histogram(
"queue.size",
8.0,
asList(50.0, 100.0),
asList(3.0, 5.0),
750.0,
10.0,
95.0)))),

// ── histogram — with explicit bounds, overflow, and attrs ─────────────
Arguments.of(
"histogram with bounds and string attr",
"histogram with bounds, overflow, and string attr",
asList(
scope(
"io.hist",
histogram(
"response.time",
10.0,
asList(1.0, 5.0, 10.0),
asList(1.0, 5.0, 10.0, Double.POSITIVE_INFINITY),
asList(2.0, 3.0, 4.0, 1.0),
45.5,
0.5,
12.0,
strAttr("region", "us-east"))))),

// ── histogram — many buckets with multiple attrs ───────────────────────
// ── histogram — many buckets with overflow and multiple attrs ──────────
Arguments.of(
"histogram many buckets with long and bool attrs",
"histogram many buckets with overflow, long and bool attrs",
asList(
scope(
"io.hist",
histogram(
"latency.ms",
100.0,
asList(1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0),
asList(1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, Double.POSITIVE_INFINITY),
asList(5.0, 10.0, 20.0, 30.0, 15.0, 12.0, 6.0, 2.0),
4321.0,
0.5,
Expand Down Expand Up @@ -985,28 +1014,69 @@ private static void verifyHistogramDataPoint(CodedInputStream dp, MetricSpec exp
assertTrue(foundMin, "min required for histogram " + expected.name);
assertTrue(foundMax, "max required for histogram " + expected.name);

assertEquals(
hp.bucketCounts.size(),
parsedBucketCounts.size(),
"bucket_counts size for " + expected.name);
for (int i = 0; i < hp.bucketCounts.size(); i++) {
// Input uses equal counts and boundaries; derive the expected OTLP wire output:
// - finite boundaries are written as explicit_bounds (+Infinity overflow marker is dropped)
// - when there is no overflow boundary, one extra zero count is appended so that
// bucket_counts.size() == explicit_bounds.size() + 1, as required by the OTLP spec
List<Double> expectedBounds = new ArrayList<>();
boolean hasOverflow = false;
for (double b : hp.bucketBoundaries) {
if (Double.isInfinite(b)) {
hasOverflow = true;
} else {
expectedBounds.add(b);
}
}
List<Long> expectedCounts = new ArrayList<>();
for (double c : hp.bucketCounts) {
expectedCounts.add((long) c);
}
if (!expectedCounts.isEmpty() && !hasOverflow) {
expectedCounts.add(0L);
}

// OTLP spec: bucket_counts.size() == explicit_bounds.size() + 1, or both 0
if (expectedCounts.isEmpty()) {
assertEquals(
(long) hp.bucketCounts.get(i).doubleValue(),
(long) parsedBucketCounts.get(i),
"bucket_counts[" + i + "] for " + expected.name);
0,
parsedBounds.size(),
"explicit_bounds must be 0 when bucket_counts is 0 for " + expected.name);
assertEquals(
0,
parsedBucketCounts.size(),
"bucket_counts must be 0 when explicit_bounds is 0 for " + expected.name);
} else {
assertEquals(
parsedBounds.size() + 1,
parsedBucketCounts.size(),
"OTLP spec: bucket_counts must be explicit_bounds + 1 for " + expected.name);
}

// +Infinity must never appear as an explicit bound on the wire
assertFalse(
parsedBounds.stream().anyMatch(b -> Double.isInfinite(b)),
"+Infinity must not appear in explicit_bounds for " + expected.name);

assertEquals(
hp.bucketBoundaries.size(),
parsedBounds.size(),
"explicit_bounds size for " + expected.name);
for (int i = 0; i < hp.bucketBoundaries.size(); i++) {
expectedBounds.size(), parsedBounds.size(), "explicit_bounds size for " + expected.name);
for (int i = 0; i < expectedBounds.size(); i++) {
assertEquals(
Double.doubleToRawLongBits(hp.bucketBoundaries.get(i)),
Double.doubleToRawLongBits(expectedBounds.get(i)),
Double.doubleToRawLongBits(parsedBounds.get(i)),
"explicit_bounds[" + i + "] for " + expected.name);
}

assertEquals(
expectedCounts.size(),
parsedBucketCounts.size(),
"bucket_counts size for " + expected.name);
for (int i = 0; i < expectedCounts.size(); i++) {
assertEquals(
(long) expectedCounts.get(i),
(long) parsedBucketCounts.get(i),
"bucket_counts[" + i + "] for " + expected.name);
}

assertEquals(
expected.attrs.size(),
parsedAttrKeys.size(),
Expand Down
Loading