Skip to content

Commit

Permalink
Merge pull request #354 from iRevive/sdk-trace/instrumentation-scope
Browse files Browse the repository at this point in the history
sdk-common: add `InstrumentationScope`
  • Loading branch information
iRevive authored Nov 9, 2023
2 parents 1fa27cd + b842e24 commit 9aff477
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ lazy val `sdk-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.dependsOn(`sdk-common`, `core-trace`)
.settings(
name := "otel4s-sdk-trace",
startYear := Some(2023),
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % CatsEffectVersion,
"org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* 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 common

import cats.Hash
import cats.Show
import cats.syntax.show._

/** Holds information about instrumentation scope.
*
* Instrumentation scope is a logical unit of the application code with which
* emitted telemetry is associated. The most common approach is to use the
* instrumentation library as the scope, however other scopes are also common,
* e.g. a module, a package, or a class may be chosen as the instrumentation
* scope.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/glossary/#instrumentation-scope Instrumentation Scope spec]]
*/
sealed trait InstrumentationScope {

/** Returns the name of the instrumentation scope.
*/
def name: String

/** Returns the version of the instrumentation scope, or None if not
* available.
*/
def version: Option[String]

/** Returns the URL of the schema used by this instrumentation scope, or None
* if not available.
*/
def schemaUrl: Option[String]

/** Returns the attributes of this instrumentation scope.
*/
def attributes: Attributes

override final def hashCode(): Int =
Hash[InstrumentationScope].hash(this)

override final def equals(obj: Any): Boolean =
obj match {
case other: InstrumentationScope =>
Hash[InstrumentationScope].eqv(this, other)
case _ =>
false
}

override final def toString: String =
Show[InstrumentationScope].show(this)
}

object InstrumentationScope {

/** A builder of [[InstrumentationScope]].
*/
sealed trait Builder {

/** Assigns the version to the scope.
*
* @param version
* the version to assign
*/
def withVersion(version: String): Builder

/** Assigns the schema URL to the scope.
*
* @param schemaUrl
* the schema URL to assign
*/
def withSchemaUrl(schemaUrl: String): Builder

/** Assigns the attributes to the scope.
*
* '''Note''': if called multiple times, only the last specified attributes
* will be used.
*
* @param attributes
* the attributes to assign
*/
def withAttributes(attributes: Attributes): Builder

/** Creates an [[InstrumentationScope]] with the configuration of this
* builder.
*/
def build: InstrumentationScope
}

private val Empty: InstrumentationScope =
apply("", None, None, Attributes.Empty)

/** An empty [[InstrumentationScope]] */
def empty: InstrumentationScope = Empty

/** Creates a [[Builder]] of [[InstrumentationScope]].
*
* @param name
* the name of the instrumentation scope
*/
def builder(name: String): Builder =
ScopeImpl(name, None, None, Attributes.Empty)

/** Creates an [[InstrumentationScope]].
*
* @param name
* the name of the instrumentation scope
*
* @param version
* the version of the instrumentation scope if the scope has a version
* (e.g. a library version)
*
* @param schemaUrl
* the Schema URL that should be recorded in the emitted telemetry
*
* @param attributes
* the instrumentation scope attributes to associate with emitted telemetry
*/
def apply(
name: String,
version: Option[String],
schemaUrl: Option[String],
attributes: Attributes
): InstrumentationScope =
ScopeImpl(name, version, schemaUrl, attributes)

implicit val showInstrumentationScope: Show[InstrumentationScope] =
Show.show { scope =>
show"InstrumentationScope{name=${scope.name}, version=${scope.version}, schemaUrl=${scope.schemaUrl}, attributes=${scope.attributes}}"
}

implicit val hashInstrumentationScope: Hash[InstrumentationScope] =
Hash.by { scope =>
(scope.name, scope.version, scope.schemaUrl, scope.attributes)
}

private final case class ScopeImpl(
name: String,
version: Option[String],
schemaUrl: Option[String],
attributes: Attributes
) extends InstrumentationScope
with Builder {
def withVersion(version: String): Builder =
copy(version = Some(version))

def withSchemaUrl(schemaUrl: String): Builder =
copy(schemaUrl = Some(schemaUrl))

def withAttributes(attributes: Attributes): Builder =
copy(attributes = attributes)

def build: InstrumentationScope =
this
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.common

import cats.Show
import cats.kernel.laws.discipline.HashTests
import cats.syntax.show._
import munit._
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.sdk.arbitrary.{attributes => attributesArbitrary}
import org.typelevel.otel4s.sdk.arbitrary.attributesCogen

class InstrumentationScopeSuite extends DisciplineSuite {

private val scopeGen: Gen[InstrumentationScope] =
for {
name <- Gen.alphaNumStr
version <- Gen.option(Gen.alphaNumStr)
schemaUrl <- Gen.option(Gen.alphaNumStr)
attributes <- attributesArbitrary.arbitrary
} yield InstrumentationScope(name, version, schemaUrl, attributes)

private implicit val scopeArbitrary: Arbitrary[InstrumentationScope] =
Arbitrary(scopeGen)

private implicit val instrumentationScopeCogen: Cogen[InstrumentationScope] =
Cogen[(String, Option[String], Option[String], Attributes)].contramap { s =>
(s.name, s.version, s.schemaUrl, s.attributes)
}

checkAll(
"InstrumentationScope.HashLaws",
HashTests[InstrumentationScope].hash
)

property("Show[InstrumentationScope]") {
Prop.forAll(scopeGen) { scope =>
val expected =
show"InstrumentationScope{name=${scope.name}, version=${scope.version}, schemaUrl=${scope.schemaUrl}, attributes=${scope.attributes}}"

assertEquals(Show[InstrumentationScope].show(scope), expected)
}
}

property("create via builder") {
Prop.forAll(scopeGen) { scope =>
val builder = InstrumentationScope
.builder(scope.name)
.withAttributes(scope.attributes)

val withVersion =
scope.version.fold(builder)(builder.withVersion)

val withResource =
scope.schemaUrl.fold(withVersion)(withVersion.withSchemaUrl)

assertEquals(withResource.build, scope)
}
}

test("empty instance") {
val expected = InstrumentationScope("", None, None, Attributes.Empty)
assertEquals(InstrumentationScope.empty, expected)
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Typelevel
* 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Typelevel
* 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.
Expand Down

0 comments on commit 9aff477

Please sign in to comment.