Skip to content

Commit

Permalink
Merge pull request #596 from iRevive/sdk-metrics/exemplar-filter
Browse files Browse the repository at this point in the history
sdk-metrics: add `ExemplarFilter` and `TraceContextLookup`
  • Loading branch information
iRevive authored Apr 16, 2024
2 parents 65e3322 + a433efe commit 2c68166
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ object ExemplarData {
sealed trait DoubleExemplar extends ExemplarData { type Value = Double }

/** The trace information.
*
* [[TraceContext]] is a minimal version of SpanContext. That way,
* `sdk-metrics` does not need to depend on the `core-trace`.
*/
sealed trait TraceContext {
def traceId: ByteVector
def spanId: ByteVector
def isSampled: Boolean

override final def hashCode(): Int =
Hash[TraceContext].hash(this)
Expand All @@ -95,22 +99,27 @@ object ExemplarData {

/** Creates a [[TraceContext]] with the given `traceId` and `spanId`.
*/
def apply(traceId: ByteVector, spanId: ByteVector): TraceContext =
Impl(traceId, spanId)
def apply(
traceId: ByteVector,
spanId: ByteVector,
sampled: Boolean
): TraceContext =
Impl(traceId, spanId, sampled)

implicit val traceContextShow: Show[TraceContext] =
Show.show { c =>
s"TraceContext{traceId=${c.traceId.toHex}, spanId=${c.spanId.toHex}}"
s"TraceContext{traceId=${c.traceId.toHex}, spanId=${c.spanId.toHex}, isSampled=${c.isSampled}}"
}

implicit val traceContextHash: Hash[TraceContext] = {
implicit val byteVectorHash: Hash[ByteVector] = Hash.fromUniversalHashCode
Hash.by(c => (c.traceId, c.spanId))
Hash.by(c => (c.traceId, c.spanId, c.isSampled))
}

private final case class Impl(
traceId: ByteVector,
spanId: ByteVector
spanId: ByteVector,
isSampled: Boolean
) extends TraceContext
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2024 Typelevel
*
* 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
*
* http://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 org.typelevel.otel4s.sdk.metrics.exemplar

import org.typelevel.otel4s.Attributes
import org.typelevel.otel4s.metrics.MeasurementValue
import org.typelevel.otel4s.sdk.context.Context

/** Exemplar filters are used to pre-filter measurements before attempting to
* store them in a reservoir.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/metrics/sdk/#exemplarfilter]]
*/
sealed trait ExemplarFilter {

/** Returns whether or not a reservoir should attempt to sample a measurement.
*/
def shouldSample[A: MeasurementValue](
value: A,
attributes: Attributes,
context: Context
): Boolean
}

object ExemplarFilter {
private val AlwaysOn = new Const(decision = true)
private val AlwaysOff = new Const(decision = false)

/** A filter which makes all measurements eligible for being an exemplar.
*/
def alwaysOn: ExemplarFilter = AlwaysOn

/** A filter which makes no measurements eligible for being an exemplar.
*/
def alwaysOff: ExemplarFilter = AlwaysOff

/** A filter that only accepts measurements where there is a span in a context
* that is being sampled.
*/
def traceBased(lookup: TraceContextLookup): ExemplarFilter =
new TraceBased(lookup)

private final class Const(decision: Boolean) extends ExemplarFilter {
def shouldSample[A: MeasurementValue](
value: A,
attributes: Attributes,
context: Context
): Boolean =
decision
}

private final class TraceBased(
lookup: TraceContextLookup
) extends ExemplarFilter {
def shouldSample[A: MeasurementValue](
value: A,
attributes: Attributes,
context: Context
): Boolean =
lookup.get(context).exists(_.isSampled)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2024 Typelevel
*
* 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
*
* http://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 org.typelevel.otel4s.sdk.metrics.exemplar

import org.typelevel.otel4s.sdk.context.Context
import org.typelevel.otel4s.sdk.metrics.data.ExemplarData.TraceContext

/** Provides a way to extract `TraceContext` from the `Context`.
*/
trait TraceContextLookup {
def get(context: Context): Option[TraceContext]
}

object TraceContextLookup {
def noop: TraceContextLookup = Noop

private object Noop extends TraceContextLookup {
def get(context: Context): Option[TraceContext] = None
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2024 Typelevel
*
* 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
*
* http://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 org.typelevel.otel4s.sdk.metrics.exemplar

import cats.effect.SyncIO
import munit.ScalaCheckSuite
import org.scalacheck.Gen
import org.scalacheck.Prop
import org.typelevel.otel4s.sdk.context.Context
import org.typelevel.otel4s.sdk.metrics.data.ExemplarData
import org.typelevel.otel4s.sdk.metrics.scalacheck.Gens

class ExemplarFilterSuite extends ScalaCheckSuite {

private val valueGen: Gen[Either[Long, Double]] =
Gen.either(Gen.long, Gen.double)

private val traceContextKey = Context.Key
.unique[SyncIO, ExemplarData.TraceContext]("trace-context")
.unsafeRunSync()

test("alwaysOn - allow all values") {
val filter = ExemplarFilter.alwaysOn

Prop.forAll(valueGen, Gens.attributes) {
case (Left(value), attributes) =>
assert(filter.shouldSample(value, attributes, Context.root))

case (Right(value), attributes) =>
assert(filter.shouldSample(value, attributes, Context.root))
}
}

test("alwaysOff - forbid all values") {
val filter = ExemplarFilter.alwaysOff

Prop.forAll(valueGen, Gens.attributes) {
case (Left(value), attributes) =>
assert(!filter.shouldSample(value, attributes, Context.root))

case (Right(value), attributes) =>
assert(!filter.shouldSample(value, attributes, Context.root))
}
}

test("traceBased - forbid all values when TraceContextLookup is noop") {
val filter = ExemplarFilter.traceBased(TraceContextLookup.noop)

Prop.forAll(valueGen, Gens.attributes) {
case (Left(value), attributes) =>
assert(!filter.shouldSample(value, attributes, Context.root))

case (Right(value), attributes) =>
assert(!filter.shouldSample(value, attributes, Context.root))
}
}

test("traceBased - decide according to the tracing context") {
val filter = ExemplarFilter.traceBased(_.get(traceContextKey))

Prop.forAll(valueGen, Gens.attributes, Gens.traceContext) {
case (Left(value), attributes, traceContext) =>
val ctx = Context.root.updated(traceContextKey, traceContext)

assertEquals(
filter.shouldSample(value, attributes, ctx),
traceContext.isSampled
)

case (Right(value), attributes, traceContext) =>
val ctx = Context.root.updated(traceContextKey, traceContext)

assertEquals(
filter.shouldSample(value, attributes, ctx),
traceContext.isSampled
)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ trait Gens extends org.typelevel.otel4s.sdk.scalacheck.Gens {
for {
traceId <- Gen.stringOfN(16, Gen.hexChar)
spanId <- Gen.stringOfN(8, Gen.hexChar)
sampled <- Gen.oneOf(true, false)
} yield ExemplarData.TraceContext(
ByteVector.fromValidHex(traceId),
ByteVector.fromValidHex(spanId)
ByteVector.fromValidHex(spanId),
sampled
)

val longExemplarData: Gen[ExemplarData.LongExemplar] =
Expand Down

0 comments on commit 2c68166

Please sign in to comment.