From 3a3975262a438f81cc57f03b57c1f001c1e1b72a Mon Sep 17 00:00:00 2001 From: Maksym Ochenashko Date: Sat, 11 Nov 2023 09:53:24 +0200 Subject: [PATCH] sdk-trace: add `LinkData` --- .../otel4s/sdk/trace/data/LinkData.scala | 88 +++++++++++++ .../otel4s/sdk/trace/Arbitraries.scala | 38 ++++++ .../typelevel/otel4s/sdk/trace/Cogens.scala | 95 ++++++++++++++ .../org/typelevel/otel4s/sdk/trace/Gens.scala | 117 ++++++++++++++++++ .../otel4s/sdk/trace/data/LinkDataSuite.scala | 66 ++++++++++ 5 files changed, 404 insertions(+) create mode 100644 sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala create mode 100644 sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Arbitraries.scala create mode 100644 sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Cogens.scala create mode 100644 sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala create mode 100644 sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala diff --git a/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala new file mode 100644 index 000000000..7abb9ebdf --- /dev/null +++ b/sdk/trace/src/main/scala/org/typelevel/otel4s/sdk/trace/data/LinkData.scala @@ -0,0 +1,88 @@ +/* + * Copyright 2023 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 +package trace +package data + +import cats.Hash +import cats.Show +import cats.syntax.show._ +import org.typelevel.otel4s.trace.SpanContext + +/** Data representation of a link. + * + * Link can be also used to reference spans from the same trace. + * + * @see + * [[https://opentelemetry.io/docs/specs/otel/trace/api/#link]] + */ +sealed trait LinkData { + + /** The [[org.typelevel.otel4s.trace.SpanContext SpanContext]] of the span + * this link refers to. + */ + def spanContext: SpanContext + + /** The [[Attributes]] associated with this link. + */ + def attributes: Attributes + + override final def hashCode(): Int = + Hash[LinkData].hash(this) + + override final def equals(obj: Any): Boolean = + obj match { + case other: LinkData => Hash[LinkData].eqv(this, other) + case _ => false + } + + override final def toString: String = + Show[LinkData].show(this) +} + +object LinkData { + + /** Creates a [[LinkData]] with the given `context`. + * + * @param context + * the context of the span the link refers to + */ + def apply(context: SpanContext): LinkData = + Impl(context, Attributes.Empty) + + /** Creates a [[LinkData]] with the given `context`. + * + * @param context + * the context of the span the link refers to + */ + def apply(context: SpanContext, attributes: Attributes): LinkData = + Impl(context, attributes) + + implicit val linkDataHash: Hash[LinkData] = + Hash.by(data => (data.spanContext, data.attributes)) + + implicit val linkDataShow: Show[LinkData] = + Show.show { data => + show"LinkData{spanContext=${data.spanContext}, attributes=${data.attributes}}" + } + + private final case class Impl( + spanContext: SpanContext, + attributes: Attributes + ) extends LinkData + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Arbitraries.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Arbitraries.scala new file mode 100644 index 000000000..520e977ae --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Arbitraries.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2023 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 +package trace + +import org.scalacheck.Arbitrary +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.sdk.trace.samplers.SamplingDecision + +object Arbitraries { + + implicit val attributeArbitrary: Arbitrary[Attribute[_]] = + Arbitrary(Gens.attribute) + + implicit val attributesArbitrary: Arbitrary[Attributes] = + Arbitrary(Gens.attributes) + + implicit val resourceArbitrary: Arbitrary[Resource] = + Arbitrary(Gens.resource) + + implicit val samplingDecisionArbitrary: Arbitrary[SamplingDecision] = + Arbitrary(Gens.samplingDecision) + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Cogens.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Cogens.scala new file mode 100644 index 000000000..3132a6ea6 --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Cogens.scala @@ -0,0 +1,95 @@ +/* + * Copyright 2023 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 +package trace + +import org.scalacheck.Cogen +import org.scalacheck.rng.Seed +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.AttributeKey +import org.typelevel.otel4s.AttributeType +import org.typelevel.otel4s.sdk.trace.samplers.SamplingDecision +import org.typelevel.otel4s.trace.SpanContext +import org.typelevel.otel4s.trace.TraceFlags +import org.typelevel.otel4s.trace.TraceState + +object Cogens { + + implicit val attributeTypeCogen: Cogen[AttributeType[_]] = + Cogen[String].contramap(_.toString) + + implicit def attributeKeyCogen[A]: Cogen[AttributeKey[A]] = + Cogen[(String, String)].contramap[AttributeKey[A]] { attribute => + (attribute.name, attribute.`type`.toString) + } + + implicit def attributeCogen[A: Cogen]: Cogen[Attribute[A]] = + Cogen[(AttributeKey[A], A)].contramap(a => (a.key, a.value)) + + implicit val attributeExistentialCogen: Cogen[Attribute[_]] = + Cogen { (seed, attr) => + def primitive[A: Cogen](seed: Seed): Seed = + Cogen[A].perturb(seed, attr.value.asInstanceOf[A]) + + def list[A: Cogen](seed: Seed): Seed = + Cogen[List[A]].perturb(seed, attr.value.asInstanceOf[List[A]]) + + val valueCogen: Seed => Seed = attr.key.`type` match { + case AttributeType.Boolean => primitive[Boolean] + case AttributeType.Double => primitive[Double] + case AttributeType.String => primitive[String] + case AttributeType.Long => primitive[Long] + case AttributeType.BooleanList => list[Boolean] + case AttributeType.DoubleList => list[Double] + case AttributeType.StringList => list[String] + case AttributeType.LongList => list[Long] + } + + valueCogen(attributeKeyCogen.perturb(seed, attr.key)) + } + + implicit val attributesCogen: Cogen[Attributes] = + Cogen[List[Attribute[_]]].contramap(_.toList) + + implicit val resourceCogen: Cogen[Resource] = + Cogen[(Attributes, Option[String])].contramap { r => + (r.attributes, r.schemaUrl) + } + + implicit val samplingDecisionCogen: Cogen[SamplingDecision] = + Cogen[String].contramap(_.toString) + + implicit val traceFlagsCogen: Cogen[TraceFlags] = + Cogen[Byte].contramap(_.toByte) + + implicit val traceStateCogen: Cogen[TraceState] = + Cogen[Map[String, String]].contramap(_.asMap) + + implicit val spanContextCogen: Cogen[SpanContext] = + Cogen[(String, String, TraceFlags, TraceState, Boolean, Boolean)] + .contramap { c => + ( + c.traceIdHex, + c.spanIdHex, + c.traceFlags, + c.traceState, + c.isRemote, + c.isValid + ) + } + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala new file mode 100644 index 000000000..9fe3d7b7e --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/Gens.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2023 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 +package trace + +import org.scalacheck.Arbitrary +import org.scalacheck.Gen +import org.typelevel.otel4s.Attribute +import org.typelevel.otel4s.Attribute.KeySelect +import org.typelevel.otel4s.sdk.trace.samplers.SamplingDecision +import org.typelevel.otel4s.trace.SpanContext +import org.typelevel.otel4s.trace.SpanKind +import org.typelevel.otel4s.trace.TraceFlags +import org.typelevel.otel4s.trace.TraceState +import scodec.bits.ByteVector + +object Gens { + + private val nonEmptyString: Gen[String] = + Arbitrary.arbitrary[String].suchThat(_.nonEmpty) + + val attribute: Gen[Attribute[_]] = { + implicit val stringArb: Arbitrary[String] = + Arbitrary(nonEmptyString) + + implicit def listArb[A: Arbitrary]: Arbitrary[List[A]] = + Arbitrary(Gen.nonEmptyListOf(Arbitrary.arbitrary[A])) + + def attribute[A: KeySelect: Arbitrary]: Gen[Attribute[A]] = + for { + key <- nonEmptyString + value <- Arbitrary.arbitrary[A] + } yield Attribute(key, value) + + val string: Gen[Attribute[String]] = attribute[String] + val boolean: Gen[Attribute[Boolean]] = attribute[Boolean] + val long: Gen[Attribute[Long]] = attribute[Long] + val double: Gen[Attribute[Double]] = attribute[Double] + + val stringList: Gen[Attribute[List[String]]] = attribute[List[String]] + val booleanList: Gen[Attribute[List[Boolean]]] = attribute[List[Boolean]] + val longList: Gen[Attribute[List[Long]]] = attribute[List[Long]] + val doubleList: Gen[Attribute[List[Double]]] = attribute[List[Double]] + + Gen.oneOf( + boolean, + string, + long, + double, + stringList, + booleanList, + longList, + doubleList + ) + } + + val attributes: Gen[Attributes] = + for { + attributes <- Gen.listOf(attribute) + } yield Attributes(attributes: _*) + + val resource: Gen[Resource] = + for { + attributes <- Gens.attributes + schemaUrl <- Gen.option(nonEmptyString) + } yield Resource(attributes, schemaUrl) + + val samplingDecision: Gen[SamplingDecision] = + Gen.oneOf( + SamplingDecision.Drop, + SamplingDecision.RecordOnly, + SamplingDecision.RecordAndSample + ) + + val spanKind: Gen[SpanKind] = + Gen.oneOf( + SpanKind.Internal, + SpanKind.Server, + SpanKind.Client, + SpanKind.Producer, + SpanKind.Consumer + ) + + val traceId: Gen[ByteVector] = + for { + hi <- Gen.long + lo <- Gen.long.suchThat(_ != 0) + } yield SpanContext.TraceId.fromLongs(hi, lo) + + val spanId: Gen[ByteVector] = + for { + value <- Gen.long.suchThat(_ != 0) + } yield SpanContext.SpanId.fromLong(value) + + val spanContext: Gen[SpanContext] = + for { + traceId <- traceId + spanId <- spanId + traceFlags <- Gen.oneOf(TraceFlags.Sampled, TraceFlags.Default) + remote <- Gen.oneOf(true, false) + } yield SpanContext(traceId, spanId, traceFlags, TraceState.empty, remote) + +} diff --git a/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala new file mode 100644 index 000000000..440544f86 --- /dev/null +++ b/sdk/trace/src/test/scala/org/typelevel/otel4s/sdk/trace/data/LinkDataSuite.scala @@ -0,0 +1,66 @@ +/* + * Copyright 2023 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.trace +package data + +import cats.Show +import cats.kernel.laws.discipline.HashTests +import cats.syntax.show._ +import munit.DisciplineSuite +import org.scalacheck.Arbitrary +import org.scalacheck.Cogen +import org.scalacheck.Gen +import org.scalacheck.Prop +import org.typelevel.otel4s.sdk.Attributes +import org.typelevel.otel4s.trace.SpanContext + +class LinkDataSuite extends DisciplineSuite { + import Cogens.attributesCogen + import Cogens.spanContextCogen + + private val linkDataGen: Gen[LinkData] = + for { + spanContext <- Gens.spanContext + attributes <- Gens.attributes + } yield LinkData(spanContext, attributes) + + private implicit val linkDataArbitrary: Arbitrary[LinkData] = + Arbitrary(linkDataGen) + + private implicit val linkDataCogen: Cogen[LinkData] = + Cogen[(SpanContext, Attributes)].contramap(s => + (s.spanContext, s.attributes) + ) + + checkAll("LinkData.HashLaws", HashTests[LinkData].hash) + + test("Show[LinkData]") { + Prop.forAll(linkDataGen) { data => + val expected = + show"LinkData{spanContext=${data.spanContext}, attributes=${data.attributes}}" + + assertEquals(Show[LinkData].show(data), expected) + } + } + + test("create LinkData with given arguments") { + Prop.forAll(linkDataGen) { data => + assertEquals(LinkData(data.spanContext, data.attributes), data) + } + } + +}