Skip to content

Commit 7c0ab66

Browse files
committed
refactor to enable exponential histogram implementation
1 parent faf544f commit 7c0ab66

File tree

8 files changed

+112
-61
lines changed

8 files changed

+112
-61
lines changed

reporters/kamon-opentelemetry/src/main/resources/reference.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ kamon.otel {
4242

4343
protocol = ${kamon.otel.protocol}
4444
protocol = ${?OTEL_EXPORTER_OTLP_METRICS_PROTOCOL}
45+
46+
# explicit_bucket_histogram or base2_exponential_bucket_histogram
47+
histogram-format = explicit_bucket_histogram
48+
histogram-format = ${?OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION}
4549
}
4650

4751
trace {

reporters/kamon-opentelemetry/src/main/scala/kamon/otel/MetricsConverter.scala

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@ package kamon.otel
1818
import io.opentelemetry.sdk.common.InstrumentationScopeInfo
1919
import io.opentelemetry.sdk.metrics.data._
2020
import io.opentelemetry.sdk.metrics.internal.data._
21+
import io.opentelemetry.sdk.metrics.internal.data.exponentialhistogram.{ExponentialHistogramData, ExponentialHistogramPointData, ImmutableExponentialHistogramData}
2122
import io.opentelemetry.sdk.resources.Resource
2223
import kamon.metric.Instrument.Snapshot
2324
import kamon.metric.{Distribution, MeasurementUnit, MetricSnapshot, PeriodSnapshot}
25+
import kamon.otel.HistogramFormat.{Explicit, Exponential, HistogramFormat}
26+
import org.slf4j.LoggerFactory
2427

2528
import java.lang.{Double => JDouble, Long => JLong}
2629
import java.time.Instant
27-
import java.util.{Collection => JCollection}
30+
import java.util.{Collection => JCollection, ArrayList => JArrayList}
2831
import scala.collection.JavaConverters._
2932
import scala.collection.mutable.ArrayBuffer
3033

3134
class WithResourceMetricsConverter(resource: Resource, kamonVersion: String, from: Instant, to: Instant) {
35+
private val logger = LoggerFactory.getLogger(getClass)
3236
private val fromNs = from.toEpochMilli * 1000000
3337
private val toNs = to.toEpochMilli * 1000000
3438

@@ -50,7 +54,7 @@ class WithResourceMetricsConverter(resource: Resource, kamonVersion: String, fro
5054
toString(gauge.settings.unit),
5155
toGaugeData(gauge.instruments))
5256

53-
private def toHistogramDatum(s: Snapshot[Distribution]): HistogramPointData = {
57+
private def toExplicitHistogramDatum(s: Snapshot[Distribution]): HistogramPointData = {
5458
val boundaries = ArrayBuffer.newBuilder[JDouble]
5559
val counts = ArrayBuffer.newBuilder[JLong]
5660
for (el <- s.value.bucketsIterator) {
@@ -69,14 +73,14 @@ class WithResourceMetricsConverter(resource: Resource, kamonVersion: String, fro
6973
)
7074
}
7175

72-
private def toHistogramData(distributions: Seq[Snapshot[Distribution]]): Option[HistogramData] =
76+
private def toExplicitHistogramData(distributions: Seq[Snapshot[Distribution]]): Option[HistogramData] =
7377
distributions.filter(_.value.buckets.nonEmpty) match {
7478
case Nil => None
75-
case nonEmpty => Some(ImmutableHistogramData.create(AggregationTemporality.DELTA, nonEmpty.map(toHistogramDatum).asJava))
79+
case nonEmpty => Some(ImmutableHistogramData.create(AggregationTemporality.DELTA, nonEmpty.map(toExplicitHistogramDatum).asJava))
7680
}
7781

78-
def convertHistogram(histogram: MetricSnapshot.Distributions): Option[MetricData] =
79-
toHistogramData(histogram.instruments).map(d =>
82+
def convertExplicitHistogram(histogram: MetricSnapshot.Distributions): Option[MetricData] =
83+
toExplicitHistogramData(histogram.instruments).map(d =>
8084
ImmutableMetricData.createDoubleHistogram(
8185
resource,
8286
instrumentationScopeInfo(histogram),
@@ -85,6 +89,42 @@ class WithResourceMetricsConverter(resource: Resource, kamonVersion: String, fro
8589
toString(histogram.settings.unit),
8690
d))
8791

92+
private def toExponentialHistogramData(distributions: Seq[Snapshot[Distribution]]): Option[ExponentialHistogramData] =
93+
distributions.filter(_.value.buckets.nonEmpty) match {
94+
case Nil => None
95+
case nonEmpty =>
96+
val mapped = nonEmpty.flatMap { s =>
97+
s.value match {
98+
case zigZag: Distribution.ZigZagCounts =>
99+
logger.error("Unable to construct exponential histogram data - Unimplemented")
100+
None
101+
// Some(ExponentialHistogramPointData.create(
102+
// ???, zigZag.sum, ???, ???, ???, fromNs, toNs, SpanConverter.toAttributes(s.tags), new JArrayList[DoubleExemplarData]()
103+
// ))
104+
case _ =>
105+
logger.error("Unable to construct exponential histogram data - only ZigZagCounts distribution can be converted")
106+
None
107+
}
108+
}
109+
if (mapped.nonEmpty) Some(ImmutableExponentialHistogramData.create(AggregationTemporality.DELTA, mapped.asJava))
110+
else None
111+
}
112+
113+
def convertExponentialHistogram(histogram: MetricSnapshot.Distributions): Option[MetricData] =
114+
toExponentialHistogramData(histogram.instruments).map(d =>
115+
ImmutableMetricData.createExponentialHistogram(
116+
resource,
117+
instrumentationScopeInfo(histogram),
118+
histogram.name,
119+
histogram.description,
120+
toString(histogram.settings.unit),
121+
d))
122+
123+
def convertHistogram(histogramFormat: HistogramFormat)(histogram: MetricSnapshot.Distributions): Option[MetricData] = histogramFormat match {
124+
case Explicit => convertExplicitHistogram(histogram)
125+
case Exponential => convertExponentialHistogram(histogram)
126+
}
127+
88128
private def toCounterDatum(g: Snapshot[Long]): LongPointData =
89129
ImmutableLongPointData.create(fromNs, toNs, SpanConverter.toAttributes(g.tags), g.value)
90130

@@ -106,10 +146,11 @@ class WithResourceMetricsConverter(resource: Resource, kamonVersion: String, fro
106146
* Converts Kamon metrics to OpenTelemetry [[MetricData]]s
107147
*/
108148
private[otel] object MetricsConverter {
109-
def convert(resource: Resource, kamonVersion: String)(metrics: PeriodSnapshot): JCollection[MetricData] = {
149+
def convert(resource: Resource, kamonVersion: String, histogramFormat: HistogramFormat)(metrics: PeriodSnapshot): JCollection[MetricData] = {
110150
val converter = new WithResourceMetricsConverter(resource, kamonVersion, metrics.from, metrics.to)
111151
val gauges = metrics.gauges.filter(_.instruments.nonEmpty).map(converter.convertGauge)
112-
val histograms = (metrics.histograms ++ metrics.timers ++ metrics.rangeSamplers).filter(_.instruments.nonEmpty).flatMap(converter.convertHistogram)
152+
val histograms = (metrics.histograms ++ metrics.timers ++ metrics.rangeSamplers).filter(_.instruments.nonEmpty)
153+
.flatMap(converter.convertHistogram(histogramFormat))
113154
val counters = metrics.counters.filter(_.instruments.nonEmpty).map(converter.convertCounter)
114155

115156
(gauges ++ histograms ++ counters).asJava

reporters/kamon-opentelemetry/src/main/scala/kamon/otel/OpenTelemetryConfiguration.scala

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ import kamon.status.Status
77
import kamon.tag.Tag
88
import org.slf4j.LoggerFactory
99

10-
import java.net.URL
10+
import java.net.{URL, URLDecoder}
1111
import java.time.Duration
12+
import scala.util.Try
1213

13-
case class OpenTelemetryConfiguration(protocol: String, endpoint: String, compressionEnabled: Boolean, headers: Seq[(String, String)], timeout: Duration)
14+
object HistogramFormat extends Enumeration {
15+
val Explicit, Exponential = Value
16+
type HistogramFormat = Value
17+
}
18+
import HistogramFormat._
19+
20+
case class OpenTelemetryConfiguration(protocol: String, endpoint: String, compressionEnabled: Boolean, headers: Seq[(String, String)], timeout: Duration, histogramFormat: Option[HistogramFormat])
1421

1522
object OpenTelemetryConfiguration {
1623
private val logger = LoggerFactory.getLogger(classOf[OpenTelemetryConfiguration])
@@ -66,13 +73,21 @@ object OpenTelemetryConfiguration {
6673
case (_, Some(full)) => full
6774
case (_, None) => endpoint
6875
}
76+
val histogramFormat = if (component == Metrics) Some(otelExporterConfig.getString("histogram-format").toLowerCase match {
77+
case "explicit_bucket_histogram" => Explicit
78+
case "base2_exponential_bucket_histogram" => Exponential
79+
case x =>
80+
logger.warn(s"unrecognised histogram-format $x. Defaulting to Explicit")
81+
Explicit
82+
}) else None
6983

7084
logger.info(s"Configured endpoint for OpenTelemetry $name reporting [$url] using $protocol protocol")
7185

72-
OpenTelemetryConfiguration(protocol, url, compression, headers, timeout)
86+
OpenTelemetryConfiguration(protocol, url, compression, headers, timeout, histogramFormat)
7387
}
7488

7589
private val kamonSettings: Status.Settings = Kamon.status().settings()
90+
7691
/**
7792
* Builds the resource information added as resource labels to the exported metrics/traces
7893
*
@@ -98,4 +113,12 @@ object OpenTelemetryConfiguration {
98113

99114
builder.build()
100115
}
116+
117+
def getAttributes(config: Config): Map[String, String] =
118+
config.getString("kamon.otel.attributes").split(',').filter(_ contains '=').map(_.trim.split("=", 2)).map {
119+
case Array(k, v) =>
120+
val decoded = Try(URLDecoder.decode(v.trim, "UTF-8"))
121+
decoded.failed.foreach(t => throw new IllegalArgumentException(s"value for attribute ${k.trim} is not a url-encoded string", t))
122+
k.trim -> decoded.get
123+
}.toMap
101124
}

reporters/kamon-opentelemetry/src/main/scala/kamon/otel/OpenTelemetryMetricsReporter.scala

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import io.opentelemetry.sdk.resources.Resource
2121
import kamon.Kamon
2222
import kamon.metric.PeriodSnapshot
2323
import kamon.module.{MetricReporter, Module, ModuleFactory}
24+
import kamon.otel.HistogramFormat.Explicit
25+
import kamon.otel.OpenTelemetryConfiguration.Component.Metrics
2426
import kamon.status.Status
25-
import kamon.tag.Tag
2627
import org.slf4j.LoggerFactory
2728

28-
import java.net.URLDecoder
2929
import java.util
3030
import java.util.{Collection => JCollection}
3131
import scala.concurrent.ExecutionContext
32-
import scala.util.{Failure, Success, Try}
32+
import scala.util.{Failure, Success}
3333

3434
object OpenTelemetryMetricsReporter {
3535
private val logger = LoggerFactory.getLogger(classOf[OpenTelemetryMetricsReporter])
@@ -51,7 +51,7 @@ import kamon.otel.OpenTelemetryMetricsReporter._
5151
/**
5252
* Converts internal Kamon metrics to OpenTelemetry format and sends to a configured OpenTelemetry endpoint using gRPC or REST.
5353
*/
54-
class OpenTelemetryMetricsReporter(metricsServiceFactory: Config => MetricsService)(implicit ec: ExecutionContext) extends MetricReporter {
54+
class OpenTelemetryMetricsReporter(metricsServiceFactory: OpenTelemetryConfiguration => MetricsService)(implicit ec: ExecutionContext) extends MetricReporter {
5555
private var metricsService: Option[MetricsService] = None
5656
private var metricsConverterFunc: PeriodSnapshot => JCollection[MetricData] = (_ => new util.ArrayList[MetricData](0))
5757

@@ -74,17 +74,16 @@ class OpenTelemetryMetricsReporter(metricsServiceFactory: Config => MetricsServi
7474
logger.info("Reconfigure OpenTelemetry Metrics Reporter")
7575

7676
//pre-generate the function for converting Kamon metrics to proto metrics
77-
val attributes: Map[String, String] =
78-
newConfig.getString("kamon.otel.attributes").split(',').filter(_ contains '=').map(_.trim.split("=", 2)).map {
79-
case Array(k, v) =>
80-
val decoded = Try(URLDecoder.decode(v.trim, "UTF-8"))
81-
decoded.failed.foreach(t => throw new IllegalArgumentException(s"value for attribute ${k.trim} is not a url-encoded string", t))
82-
k.trim -> decoded.get
83-
}.toMap
77+
val attributes: Map[String, String] = OpenTelemetryConfiguration.getAttributes(newConfig)
8478
val resource: Resource = OpenTelemetryConfiguration.buildResource(attributes)
85-
this.metricsConverterFunc = MetricsConverter.convert(resource, kamonSettings.version)
79+
val config = OpenTelemetryConfiguration(newConfig, Metrics)
80+
val histogramFormat = config.histogramFormat.getOrElse {
81+
logger.warn("Missing histogram-format from metrics configuration, defaulting to Explicit")
82+
Explicit
83+
}
84+
this.metricsConverterFunc = MetricsConverter.convert(resource, kamonSettings.version, histogramFormat)
8685

87-
this.metricsService = Option(metricsServiceFactory.apply(newConfig))
86+
this.metricsService = Option(metricsServiceFactory.apply(config))
8887
}
8988

9089
override def stop(): Unit = {

reporters/kamon-opentelemetry/src/main/scala/kamon/otel/OpenTelemetryTraceReporter.scala

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,20 @@
1515
*/
1616
package kamon.otel
1717

18-
import java.util
19-
2018
import com.typesafe.config.Config
19+
import io.opentelemetry.sdk.resources.Resource
20+
import io.opentelemetry.sdk.trace.data.SpanData
2121
import kamon.Kamon
2222
import kamon.module.{Module, ModuleFactory, SpanReporter}
23+
import kamon.otel.OpenTelemetryConfiguration.Component.Trace
24+
import kamon.status.Status
2325
import kamon.trace.Span
2426
import org.slf4j.LoggerFactory
25-
import java.net.URLDecoder
26-
import java.util.{Collection => JCollection}
2727

28+
import java.util
29+
import java.util.{Collection => JCollection}
2830
import scala.concurrent.ExecutionContext
29-
import scala.util.{Failure, Success, Try}
30-
31-
import io.opentelemetry.sdk.common.InstrumentationLibraryInfo
32-
import io.opentelemetry.sdk.resources.Resource
33-
import io.opentelemetry.sdk.trace.data.SpanData
34-
import kamon.status.Status
35-
import kamon.tag.Tag
31+
import scala.util.{Failure, Success}
3632

3733
object OpenTelemetryTraceReporter {
3834
private val logger = LoggerFactory.getLogger(classOf[OpenTelemetryTraceReporter])
@@ -49,12 +45,12 @@ object OpenTelemetryTraceReporter {
4945
}
5046
}
5147

52-
import OpenTelemetryTraceReporter._
48+
import kamon.otel.OpenTelemetryTraceReporter._
5349

5450
/**
5551
* Converts internal finished Kamon spans to OpenTelemetry format and sends to a configured OpenTelemetry endpoint using gRPC or REST.
5652
*/
57-
class OpenTelemetryTraceReporter(traceServiceFactory: Config => TraceService)(implicit ec: ExecutionContext) extends SpanReporter {
53+
class OpenTelemetryTraceReporter(traceServiceFactory: OpenTelemetryConfiguration => TraceService)(implicit ec: ExecutionContext) extends SpanReporter {
5854
private var traceService: Option[TraceService] = None
5955
private var spanConverterFunc: Seq[Span.Finished] => JCollection[SpanData] = (_ => new util.ArrayList[SpanData](0))
6056

@@ -74,17 +70,12 @@ class OpenTelemetryTraceReporter(traceServiceFactory: Config => TraceService)(im
7470
logger.info("Reconfigure OpenTelemetry Trace Reporter")
7571

7672
//pre-generate the function for converting Kamon span to proto span
77-
val attributes: Map[String, String] =
78-
newConfig.getString("kamon.otel.attributes").split(',').filter(_ contains '=').map(_.trim.split("=", 2)).map {
79-
case Array(k, v) =>
80-
val decoded = Try(URLDecoder.decode(v.trim, "UTF-8"))
81-
decoded.failed.foreach(t => throw new IllegalArgumentException(s"value for attribute ${k.trim} is not a url-encoded string", t))
82-
k.trim -> decoded.get
83-
}.toMap
73+
val attributes: Map[String, String] = OpenTelemetryConfiguration.getAttributes(newConfig)
8474
val resource: Resource = OpenTelemetryConfiguration.buildResource(attributes)
75+
val config = OpenTelemetryConfiguration(newConfig, Trace)
8576
this.spanConverterFunc = SpanConverter.convert(newConfig.getBoolean("kamon.otel.trace.include-error-event"), resource, kamonSettings.version)
8677

87-
this.traceService = Option(traceServiceFactory.apply(newConfig))
78+
this.traceService = Option(traceServiceFactory.apply(config))
8879
}
8980

9081
override def stop(): Unit = {

reporters/kamon-opentelemetry/src/main/scala/kamon/otel/Services.scala

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package kamon.otel
1717

18-
import com.typesafe.config.Config
1918
import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter
2019
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
2120
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter
@@ -24,8 +23,6 @@ import io.opentelemetry.sdk.metrics.`export`.MetricExporter
2423
import io.opentelemetry.sdk.metrics.data.MetricData
2524
import io.opentelemetry.sdk.trace.`export`.SpanExporter
2625
import io.opentelemetry.sdk.trace.data.SpanData
27-
import kamon.otel.OpenTelemetryConfiguration.Component.{ Metrics, Trace }
28-
import org.slf4j.LoggerFactory
2926

3027
import java.io.Closeable
3128
import java.util.{Collection => JCollection}
@@ -43,15 +40,13 @@ private[otel] trait TraceService extends Closeable {
4340
* Companion object to [[OtlpTraceService]]
4441
*/
4542
private[otel] object OtlpTraceService {
46-
private val logger = LoggerFactory.getLogger(classOf[OtlpTraceService])
47-
4843
/**
4944
* Builds the http/protobuf trace exporter using the provided configuration.
5045
*
5146
* @param config
5247
* @return
5348
*/
54-
def apply(config: Config): TraceService = new OtlpTraceService(OpenTelemetryConfiguration(config, Trace))
49+
def apply(config: OpenTelemetryConfiguration): TraceService = new OtlpTraceService(config)
5550
}
5651

5752
private[otel] class OtlpTraceService(c: OpenTelemetryConfiguration) extends TraceService {
@@ -98,15 +93,13 @@ private[otel] trait MetricsService extends Closeable {
9893
* Companion object to [[OtlpMetricsService]]
9994
*/
10095
private[otel] object OtlpMetricsService {
101-
private val logger = LoggerFactory.getLogger(classOf[OtlpTraceService])
102-
10396
/**
10497
* Builds the http/protobuf metrics exporter using the provided configuration.
10598
*
10699
* @param config
107100
* @return
108101
*/
109-
def apply(config: Config): MetricsService = new OtlpMetricsService(OpenTelemetryConfiguration(config, Metrics))
102+
def apply(config: OpenTelemetryConfiguration): MetricsService = new OtlpMetricsService(config)
110103
}
111104

112105
private[otel] class OtlpMetricsService(c: OpenTelemetryConfiguration) extends MetricsService {

reporters/kamon-opentelemetry/src/test/scala/kamon/otel/OpenTelemetryMetricReporterSpec.scala

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package kamon.otel
1818

19-
import com.typesafe.config.{Config, ConfigFactory}
2019
import io.opentelemetry.api.common.AttributeKey
2120
import io.opentelemetry.sdk.metrics.data.MetricData
2221
import kamon.Kamon
@@ -27,7 +26,7 @@ import kamon.testkit.Reconfigure
2726
import org.scalatest.matchers.should.Matchers
2827
import org.scalatest.wordspec.AnyWordSpec
2928

30-
import java.lang.{Double => JDouble, Long => JLong}
29+
import java.lang.{Double => JDouble}
3130
import java.time.Instant
3231
import java.util.{Collection => JCollection}
3332
import scala.collection.JavaConverters._
@@ -90,7 +89,7 @@ class OpenTelemetryMetricReporterSpec extends AnyWordSpec
9089

9190
//assert instrumentation labels
9291
val instrumentationScopeInfo = metricData.getInstrumentationScopeInfo
93-
instrumentationScopeInfo.getName should be("kamon-instrumentation")
92+
instrumentationScopeInfo.getName should be("kamon-metrics")
9493
instrumentationScopeInfo.getVersion should be(kamonVersion)
9594
instrumentationScopeInfo.getSchemaUrl should be(null)
9695

0 commit comments

Comments
 (0)