-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Stackdriver Distribution count and bucket count sum mismatch #5836
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* Copyright 2025 VMware, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.micrometer.stackdriver; | ||
|
||
import io.micrometer.core.instrument.Clock; | ||
import io.micrometer.core.instrument.distribution.*; | ||
import io.micrometer.core.instrument.step.StepDistributionSummary; | ||
|
||
import static io.micrometer.stackdriver.StackdriverHistogramUtil.stackdriverHistogram; | ||
|
||
class StackdriverDistributionSummary extends StepDistributionSummary { | ||
|
||
public StackdriverDistributionSummary(Id id, Clock clock, DistributionStatisticConfig distributionStatisticConfig, | ||
double scale, long stepMillis) { | ||
super(id, clock, distributionStatisticConfig, scale, stepMillis, | ||
stackdriverHistogram(clock, distributionStatisticConfig)); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* Copyright 2025 VMware, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.micrometer.stackdriver; | ||
|
||
import io.micrometer.core.instrument.Clock; | ||
import io.micrometer.core.instrument.distribution.*; | ||
|
||
final class StackdriverHistogramUtil { | ||
|
||
private StackdriverHistogramUtil() { | ||
} | ||
|
||
// copied and modified from AbstractDistributionSummary/AbstractTimer | ||
static Histogram stackdriverHistogram(Clock clock, DistributionStatisticConfig distributionStatisticConfig) { | ||
if (distributionStatisticConfig.isPublishingPercentiles()) { | ||
return new StackdriverClientSidePercentilesHistogram(clock, distributionStatisticConfig); | ||
} | ||
if (distributionStatisticConfig.isPublishingHistogram()) { | ||
return new StackdriverFixedBoundaryHistogram(clock, distributionStatisticConfig); | ||
} | ||
return NoopHistogram.INSTANCE; | ||
} | ||
|
||
static class StackdriverClientSidePercentilesHistogram extends TimeWindowPercentileHistogram { | ||
|
||
StackdriverClientSidePercentilesHistogram(Clock clock, | ||
DistributionStatisticConfig distributionStatisticConfig) { | ||
super(clock, distributionStatisticConfig, true, false, true); | ||
} | ||
|
||
} | ||
|
||
static class StackdriverFixedBoundaryHistogram extends TimeWindowFixedBoundaryHistogram { | ||
|
||
StackdriverFixedBoundaryHistogram(Clock clock, DistributionStatisticConfig config) { | ||
super(clock, config, true, false, true); | ||
} | ||
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,9 +32,7 @@ | |
import io.micrometer.core.instrument.distribution.HistogramSnapshot; | ||
import io.micrometer.core.instrument.distribution.HistogramSupport; | ||
import io.micrometer.core.instrument.distribution.pause.PauseDetector; | ||
import io.micrometer.core.instrument.step.StepDistributionSummary; | ||
import io.micrometer.core.instrument.step.StepMeterRegistry; | ||
import io.micrometer.core.instrument.step.StepTimer; | ||
import io.micrometer.core.instrument.util.DoubleFormat; | ||
import io.micrometer.core.instrument.util.NamedThreadFactory; | ||
import org.slf4j.Logger; | ||
|
@@ -46,7 +44,6 @@ | |
import java.util.concurrent.ThreadFactory; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicLong; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
|
@@ -289,15 +286,15 @@ Stream<TimeSeries> createLongTaskTimer(Batch batch, LongTaskTimer longTaskTimer) | |
@Override | ||
protected DistributionSummary newDistributionSummary(Meter.Id id, | ||
DistributionStatisticConfig distributionStatisticConfig, double scale) { | ||
return new StepDistributionSummary(id, clock, distributionStatisticConfig, scale, config.step().toMillis(), | ||
true); | ||
return new StackdriverDistributionSummary(id, clock, distributionStatisticConfig, scale, | ||
config.step().toMillis()); | ||
} | ||
|
||
@Override | ||
protected Timer newTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig, | ||
PauseDetector pauseDetector) { | ||
return new StepTimer(id, clock, distributionStatisticConfig, pauseDetector, getBaseTimeUnit(), | ||
this.config.step().toMillis(), true); | ||
return new StackdriverTimer(id, clock, distributionStatisticConfig, pauseDetector, getBaseTimeUnit(), | ||
this.config.step().toMillis()); | ||
} | ||
|
||
@Override | ||
|
@@ -508,18 +505,17 @@ private TimeInterval interval(MetricDescriptor.MetricKind metricKind) { | |
Distribution distribution(HistogramSnapshot snapshot, boolean timeDomain) { | ||
CountAtBucket[] histogram = snapshot.histogramCounts(); | ||
|
||
// selected finite buckets (represented as a normal histogram) | ||
AtomicLong truncatedSum = new AtomicLong(); | ||
AtomicReference<Double> last = new AtomicReference<>(0.0); | ||
List<Long> bucketCounts = Arrays.stream(histogram).map(countAtBucket -> { | ||
double cumulativeCount = countAtBucket.count(); | ||
long bucketCount = (long) (cumulativeCount - last.getAndSet(cumulativeCount)); | ||
truncatedSum.addAndGet(bucketCount); | ||
return bucketCount; | ||
}).collect(toCollection(ArrayList::new)); | ||
|
||
if (!bucketCounts.isEmpty()) { | ||
int endIndex = bucketCounts.size() - 1; | ||
List<Long> bucketCounts = Arrays.stream(histogram) | ||
.map(CountAtBucket::count) | ||
.map(Double::longValue) | ||
.collect(toCollection(ArrayList::new)); | ||
long cumulativeCount = Arrays.stream(histogram).mapToLong(c -> (long) c.count()).sum(); | ||
|
||
// no-op histogram will have no buckets; other histograms should have at least | ||
// the +Inf bucket | ||
if (!bucketCounts.isEmpty() && bucketCounts.size() > 1) { | ||
// the rightmost bucket should be the infinity bucket; do not trim that | ||
int endIndex = bucketCounts.size() - 2; | ||
// trim zero-count buckets on the right side of the domain | ||
if (bucketCounts.get(endIndex) == 0) { | ||
int lastNonZeroIndex = 0; | ||
|
@@ -529,30 +525,41 @@ Distribution distribution(HistogramSnapshot snapshot, boolean timeDomain) { | |
break; | ||
} | ||
} | ||
long infCount = bucketCounts.get(bucketCounts.size() - 1); | ||
bucketCounts = bucketCounts.subList(0, lastNonZeroIndex + 1); | ||
// infinite bucket count of 0 can be omitted | ||
bucketCounts.add(infCount); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I left this note here but did not omit 0 count infinity buckets to try to keep the behavior the same as before since this is a change in a maintenance branch. |
||
} | ||
} | ||
|
||
// add the "+infinity" bucket, which does NOT have a corresponding bucket | ||
// boundary | ||
bucketCounts.add(Math.max(0, snapshot.count() - truncatedSum.get())); | ||
// no-op histogram | ||
if (bucketCounts.isEmpty()) { | ||
shakuzen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bucketCounts.add(0L); | ||
} | ||
|
||
List<Double> bucketBoundaries = Arrays.stream(histogram) | ||
.map(countAtBucket -> timeDomain ? countAtBucket.bucket(getBaseTimeUnit()) : countAtBucket.bucket()) | ||
.collect(toCollection(ArrayList::new)); | ||
|
||
if (bucketBoundaries.size() == 1) { | ||
bucketBoundaries.remove(Double.POSITIVE_INFINITY); | ||
} | ||
|
||
// trim bucket boundaries to match bucket count trimming | ||
if (bucketBoundaries.size() != bucketCounts.size() - 1) { | ||
bucketBoundaries = bucketBoundaries.subList(0, bucketCounts.size() - 1); | ||
} | ||
|
||
// stackdriver requires at least one finite bucket | ||
// Stackdriver requires at least one explicit bucket bound | ||
if (bucketBoundaries.isEmpty()) { | ||
bucketBoundaries.add(0.0); | ||
} | ||
|
||
return Distribution.newBuilder() | ||
// is the mean optional? better to not send as it is for a different time | ||
// window than the histogram | ||
.setMean(timeDomain ? snapshot.mean(getBaseTimeUnit()) : snapshot.mean()) | ||
.setCount(snapshot.count()) | ||
.setCount(cumulativeCount) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the part that fixes things - |
||
.setBucketOptions(Distribution.BucketOptions.newBuilder() | ||
.setExplicitBuckets( | ||
Distribution.BucketOptions.Explicit.newBuilder().addAllBounds(bucketBoundaries).build()) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* Copyright 2025 VMware, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package io.micrometer.stackdriver; | ||
|
||
import io.micrometer.core.instrument.Clock; | ||
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; | ||
import io.micrometer.core.instrument.distribution.pause.PauseDetector; | ||
import io.micrometer.core.instrument.step.StepTimer; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
import static io.micrometer.stackdriver.StackdriverHistogramUtil.stackdriverHistogram; | ||
|
||
class StackdriverTimer extends StepTimer { | ||
|
||
public StackdriverTimer(Id id, Clock clock, DistributionStatisticConfig distributionStatisticConfig, | ||
PauseDetector pauseDetector, TimeUnit baseTimeUnit, long stepDurationMillis) { | ||
super(id, clock, distributionStatisticConfig, pauseDetector, baseTimeUnit, stepDurationMillis, | ||
stackdriverHistogram(clock, distributionStatisticConfig)); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We no longer need to convert cumulative buckets to non-cumulative because with the change to dedicated Stackdriver types for Timer and DistributionSummary, we pass the flag for non-cumulative histogram.