diff --git a/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionBuilder.java b/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionBuilder.java new file mode 100644 index 00000000000..93faa49aca3 --- /dev/null +++ b/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionBuilder.java @@ -0,0 +1,64 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.profiles; + +import io.opentelemetry.exporter.otlp.internal.data.ImmutableSampleData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Assembles a collection of state observations into a collection of SampleData objects i.e. proto + * Sample messages. + * + *

Observations (samples or traces from a profiler) having the same key can be merged to save + * space without loss of fidelity. On the wire, a single Sample, modelled in the API as a SampleData + * object, comprises shared fields (the key) and per-occurrence fields (the value and timestamp). + * This class maps the raw observations to the aggregations. + * + *

This class is not threadsafe and must be externally synchronized. + */ +public class SampleCompositionBuilder { + + private final Map map = new HashMap<>(); + + /** + * Constructs a new collection of SampleData instances based on the builder's value. + * + * @return a new {@code List} + */ + public List build() { + List result = new ArrayList<>(map.size()); + for (Map.Entry entry : map.entrySet()) { + SampleCompositionKey key = entry.getKey(); + SampleCompositionValue value = entry.getValue(); + SampleData sampleData = + ImmutableSampleData.create( + key.getStackIndex(), + value.getValues(), + key.getAttributeIndices(), + key.getLinkIndex(), + value.getTimestamps()); + result.add(sampleData); + } + + return result; + } + + /** + * Adds a new observation to the collection. + * + * @param key the shared ('primary key') fields of the observation. + * @param value the observed data point. + * @param timestamp the time of the observation. + */ + public void add(SampleCompositionKey key, @Nullable Long value, @Nullable Long timestamp) { + SampleCompositionValue v = map.computeIfAbsent(key, key1 -> new SampleCompositionValue()); + v.add(value, timestamp); + } +} diff --git a/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionKey.java b/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionKey.java new file mode 100644 index 00000000000..8a7abab23d4 --- /dev/null +++ b/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionKey.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.profiles; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import javax.annotation.concurrent.Immutable; + +/** + * A SampleCompositionKey represents the identity portion of an aggregation of observed data. + * Observations (samples) having the same key can be merged to save space without loss of fidelity. + */ +@Immutable +public class SampleCompositionKey { + + // on the wire, a Sample's identity (i.e. 'primary key') is the tuple of + // {stack_index, sorted(attribute_indices), link_index} + private final int stackIndex; + private final List attributeIndices; + private final int linkIndex; + + public SampleCompositionKey(int stackIndex, List attributeIndices, int linkIndex) { + this.stackIndex = stackIndex; + List tmp = new ArrayList<>(attributeIndices); + Collections.sort(tmp); + this.attributeIndices = Collections.unmodifiableList(tmp); + this.linkIndex = linkIndex; + } + + public int getStackIndex() { + return stackIndex; + } + + public List getAttributeIndices() { + return attributeIndices; + } + + public int getLinkIndex() { + return linkIndex; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SampleCompositionKey)) { + return false; + } + SampleCompositionKey that = (SampleCompositionKey) o; + return stackIndex == that.stackIndex + && linkIndex == that.linkIndex + && Objects.equals(attributeIndices, that.attributeIndices); + } + + @Override + public int hashCode() { + return Objects.hash(stackIndex, attributeIndices, linkIndex); + } +} diff --git a/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionValue.java b/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionValue.java new file mode 100644 index 00000000000..90a6041d1aa --- /dev/null +++ b/exporters/otlp/profiles/src/main/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionValue.java @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.profiles; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; + +/** + * A SampleCompositionValue represents the per-observation parts of an aggregation of observed data. + * Observations (samples) having the same key can be merged by appending their distinct fields to + * the composed value. + * + *

This class is not threadsafe and must be externally synchronized. + */ +public class SampleCompositionValue { + + private final List values = new ArrayList<>(); + private final List timestamps = new ArrayList<>(); + + public List getValues() { + return Collections.unmodifiableList(values); + } + + public List getTimestamps() { + return Collections.unmodifiableList(timestamps); + } + + /** + * Add a new observation to the collection. + * + *

Note that, whilst not enforced by the API, it is required that all observations in a + * collection share the same 'shape'. That is, they have either a value without timestamp, a + * timestamp without value, or both timestamp and value. Thus each array (values, timestamps) in + * the collection is either zero length, or the same length as the other. + * + * @param value the observed data point. + * @param timestamp the time of the observation. + */ + public void add(@Nullable Long value, @Nullable Long timestamp) { + if (value != null) { + values.add(value); + } + if (timestamp != null) { + timestamps.add(timestamp); + } + } +} diff --git a/exporters/otlp/profiles/src/test/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionTest.java b/exporters/otlp/profiles/src/test/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionTest.java new file mode 100644 index 00000000000..a4ae1f75f8a --- /dev/null +++ b/exporters/otlp/profiles/src/test/java/io/opentelemetry/exporter/otlp/profiles/SampleCompositionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.otlp.profiles; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SampleCompositionTest { + + SampleCompositionBuilder sampleCompositionBuilder; + + @BeforeEach + void setUp() { + sampleCompositionBuilder = new SampleCompositionBuilder(); + } + + @Test + public void empty() { + assertThat(sampleCompositionBuilder.build()).isEmpty(); + } + + @Test + public void keyEquality() { + SampleCompositionKey a; + SampleCompositionKey b; + + a = new SampleCompositionKey(1, listOf(2, 3), 4); + b = new SampleCompositionKey(1, listOf(2, 3), 4); + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + + b = new SampleCompositionKey(1, listOf(3, 2), 4); + assertThat(a).isEqualTo(b); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + + b = new SampleCompositionKey(2, listOf(2, 3), 4); + assertThat(a).isNotEqualTo(b); + } + + @Test + public void valueElidesNulls() { + SampleCompositionValue v = new SampleCompositionValue(); + v.add(1L, 1L); + v.add(null, 2L); + v.add(2L, null); + assertThat(v.getValues().size()).isEqualTo(2); + assertThat(v.getTimestamps().size()).isEqualTo(2); + } + + @Test + public void isAggregatingSameKey() { + SampleCompositionKey sampleCompositionKey = + new SampleCompositionKey(0, Collections.emptyList(), 0); + sampleCompositionBuilder.add(sampleCompositionKey, 1L, 1L); + sampleCompositionBuilder.add(sampleCompositionKey, 2L, 2L); + + List sampleDataList = sampleCompositionBuilder.build(); + assertThat(sampleDataList).size().isEqualTo(1); + assertThat(sampleDataList.get(0).getTimestamps().size()).isEqualTo(2); + assertThat(sampleDataList.get(0).getValues().size()).isEqualTo(2); + } + + @Test + public void isNotAggregatingDifferentKey() { + SampleCompositionKey keyA = new SampleCompositionKey(1, Collections.emptyList(), 0); + sampleCompositionBuilder.add(keyA, 1L, 1L); + SampleCompositionKey keyB = new SampleCompositionKey(2, Collections.emptyList(), 0); + sampleCompositionBuilder.add(keyB, 2L, 2L); + + List sampleDataList = sampleCompositionBuilder.build(); + assertThat(sampleDataList).size().isEqualTo(2); + assertThat(sampleDataList.get(0).getTimestamps().size()).isEqualTo(1); + assertThat(sampleDataList.get(1).getTimestamps().size()).isEqualTo(1); + } + + private static List listOf(T a, T b) { + ArrayList list = new ArrayList<>(); + list.add(a); + list.add(b); + return Collections.unmodifiableList(list); + } +}