Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Resource entity #240

Merged
merged 9 commits into from
Jul 10, 2023
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p testkit/metrics/jvm/target java/metrics/target testkit/common/jvm/target benchmarks/target examples/target core/trace/.js/target target semconv/.jvm/target core/common/.jvm/target java/trace/target unidocs/target .js/target core/metrics/.native/target core/all/.native/target site/target core/metrics/.jvm/target core/all/.js/target java/all/target java/common/target core/metrics/.js/target core/all/.jvm/target .jvm/target core/trace/.native/target .native/target semconv/.js/target core/trace/.jvm/target core/common/.native/target core/common/.js/target semconv/.native/target testkit/all/jvm/target project/target
run: mkdir -p testkit/metrics/jvm/target java/metrics/target testkit/common/jvm/target sdk/common/.native/target benchmarks/target examples/target sdk/common/.js/target core/trace/.js/target target semconv/.jvm/target core/common/.jvm/target java/trace/target unidocs/target .js/target core/metrics/.native/target core/all/.native/target site/target core/metrics/.jvm/target core/all/.js/target java/all/target java/common/target core/metrics/.js/target core/all/.jvm/target sdk/common/.jvm/target .jvm/target core/trace/.native/target .native/target semconv/.js/target core/trace/.jvm/target core/common/.native/target core/common/.js/target semconv/.native/target testkit/all/jvm/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar testkit/metrics/jvm/target java/metrics/target testkit/common/jvm/target benchmarks/target examples/target core/trace/.js/target target semconv/.jvm/target core/common/.jvm/target java/trace/target unidocs/target .js/target core/metrics/.native/target core/all/.native/target site/target core/metrics/.jvm/target core/all/.js/target java/all/target java/common/target core/metrics/.js/target core/all/.jvm/target .jvm/target core/trace/.native/target .native/target semconv/.js/target core/trace/.jvm/target core/common/.native/target core/common/.js/target semconv/.native/target testkit/all/jvm/target project/target
run: tar cf targets.tar testkit/metrics/jvm/target java/metrics/target testkit/common/jvm/target sdk/common/.native/target benchmarks/target examples/target sdk/common/.js/target core/trace/.js/target target semconv/.jvm/target core/common/.jvm/target java/trace/target unidocs/target .js/target core/metrics/.native/target core/all/.native/target site/target core/metrics/.jvm/target core/all/.js/target java/all/target java/common/target core/metrics/.js/target core/all/.jvm/target sdk/common/.jvm/target .jvm/target core/trace/.native/target .native/target semconv/.js/target core/trace/.jvm/target core/common/.native/target core/common/.js/target semconv/.native/target testkit/all/jvm/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
33 changes: 32 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ ThisBuild / tlSonatypeUseLegacyHost := false
ThisBuild / tlSitePublishBranch := Some("main")

ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0"
ThisBuild / semanticdbOptions ++= Seq("-P:semanticdb:synthetics:on").filter(_ =>
!tlIsScala3.value
)

ThisBuild / tlMimaPreviousVersions ~= (_.filterNot(_ == "0.2.0"))

Expand Down Expand Up @@ -58,6 +61,7 @@ lazy val root = tlCrossRootProject
`core-metrics`,
`core-trace`,
core,
`sdk-common`,
`testkit-common`,
`testkit-metrics`,
testkit,
Expand Down Expand Up @@ -125,6 +129,31 @@ lazy val core = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "otel4s-core"
)

lazy val `sdk-common` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Pure)
.enablePlugins(BuildInfoPlugin)
.enablePlugins(NoPublishPlugin)
.in(file("sdk/common"))
.dependsOn(`core-common`, semconv)
.settings(
name := "otel4s-sdk-common",
startYear := Some(2023),
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion,
"org.typelevel" %%% "cats-mtl" % CatsMtlVersion,
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
"org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test,
"org.scalameta" %%% "munit" % MUnitVersion % Test,
"org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test,
),
buildInfoPackage := "org.typelevel.otel4s.sdk",
buildInfoOptions += sbtbuildinfo.BuildInfoOption.PackagePrivate,
buildInfoKeys := Seq[BuildInfoKey](
version
)
)
.settings(munitDependencies)

lazy val `testkit-common` = crossProject(JVMPlatform)
.crossType(CrossType.Full)
.in(file("testkit/common"))
Expand Down Expand Up @@ -239,7 +268,8 @@ lazy val semconv = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.in(file("semconv"))
.dependsOn(`core-common`)
.settings(
name := "otel4s-semconv"
name := "otel4s-semconv",
startYear := Some(2023),
)

lazy val benchmarks = project
Expand Down Expand Up @@ -308,6 +338,7 @@ lazy val unidocs = project
`core-metrics`.jvm,
`core-trace`.jvm,
core.jvm,
`sdk-common`.jvm,
`testkit-common`.jvm,
`testkit-metrics`.jvm,
testkit.jvm,
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 Expand Up @@ -60,10 +60,7 @@ package {{pkg | trim}}
import org.typelevel.otel4s.AttributeKey
import org.typelevel.otel4s.AttributeKey._

import scala.annotation.nowarn

// DO NOT EDIT, this is an Auto-generated file from buildscripts/semantic-convention{{template}}
@nowarn("msg=never used")
object {{class}} {
/**
* The URL of the OpenTelemetry schema for these keys and values.
Expand Down Expand Up @@ -94,7 +91,7 @@ object {{class}} {
{%- if attribute.is_enum %}
{%- set class_name = attribute.fqn | to_camelcase(True) ~ "Value" %}
{%- set type = to_scala_return_type(attribute.attr_type.enum_type) %}
abstract class {{ class_name }}(value: {{ type }})
abstract class {{ class_name }}(val value: {{ type }})
object {{class_name}} {
{%- for member in attribute.attr_type.members %}
/** {% filter escape %}{{member.brief | to_doc_brief}}.{% endfilter %} */
Expand Down
12 changes: 10 additions & 2 deletions core/common/src/main/scala/org/typelevel/otel4s/Attribute.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@

package org.typelevel.otel4s

import cats.Show
import cats.implicits.showInterpolator
import cats.kernel.Hash

/** Represents the key-value attribute.
*
* @param key
* the key of the attribute. Denotes the types of the `value`
*
* @param value
* the value of the attribute
*
* @tparam A
* the type of the attribute's value. One of [[AttributeType]]
*/
Expand Down Expand Up @@ -68,4 +70,10 @@ String, Boolean, Long, Double, List[String], List[Boolean], List[Long], List[Dou
def apply[A: KeySelect](name: String, value: A): Attribute[A] =
Attribute(KeySelect[A].make(name), value)

implicit val showAttribute: Show[Attribute[_]] = (a: Attribute[_]) =>
s"${show"${a.key}"}=${a.value}"

implicit def hashAttribute[T: Hash]: Hash[Attribute[T]] =
Hash.by(a => (a.key, a.value))

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

import cats.Applicative
import cats.Monad
import cats.Monoid
import cats.Show
import cats.implicits._
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.Attribute.KeySelect
import org.typelevel.otel4s.AttributeKey

/** An immutable collection of [[Attribute]]s.
*/
final class Attributes private (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, most methods use varargs, e.g:

def resourceSpan[A](name: String, attributes: Attribute[_]*)

I wonder whether we should offer a new alternative too:

def resourceSpan[A](name: String, attributes: Attributes)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I would completely replace Attribute with Attributes. Attribute looks like an unnecessary wrapper to me because, from my perspective, users are more interested in using a collection that provides interfaces for accessing particular values by keys or iterating over all of them. I can imagine that It might be useful as a smart constructor which validates passed values, but the collection can do the same. Maybe I don't see a use-case for it.

private val m: Map[AttributeKey[_], Attribute[_]]
) {
def get[T: KeySelect](name: String): Option[Attribute[T]] = {
val key = KeySelect[T].make(name)
m.get(key).map(_.asInstanceOf[Attribute[T]])
}
def get[T](key: AttributeKey[T]): Option[Attribute[T]] =
m.get(key).map(_.asInstanceOf[Attribute[T]])

def isEmpty: Boolean = m.isEmpty
def size: Int = m.size
def contains(key: AttributeKey[_]): Boolean = m.contains(key)
def foldLeft[F[_]: Monad, B](z: B)(f: (B, Attribute[_]) => F[B]): F[B] =
m.foldLeft(Monad[F].pure(z)) { (acc, v) =>
acc.flatMap { b =>
f(b, v._2)
}
}
def forall[F[_]: Monad](p: Attribute[_] => F[Boolean]): F[Boolean] =
foldLeft(true)((b, a) => {
if (b) p(a).map(b && _)
else Monad[F].pure(false)
})
def foreach[F[_]: Applicative](f: Attribute[_] => F[Unit]): F[Unit] =
m.foldLeft(Applicative[F].unit) { (acc, v) =>
acc *> f(v._2)
}

def toMap: Map[AttributeKey[_], Attribute[_]] = m
def toList: List[Attribute[_]] = m.values.toList

}

object Attributes {

val Empty = new Attributes(Map.empty)

def apply(attributes: Attribute[_]*): Attributes = {
val builder = Map.newBuilder[AttributeKey[_], Attribute[_]]
attributes.foreach { a =>
builder += (a.key -> a)
}
new Attributes(builder.result())
}

implicit val showAttributes: Show[Attributes] = Show.show { attributes =>
attributes.toList
.map(a => show"$a")
.mkString("Attributes(", ", ", ")")
}

implicit val monoidAttributes: Monoid[Attributes] =
new Monoid[Attributes] {
def empty: Attributes = Attributes.Empty
def combine(x: Attributes, y: Attributes): Attributes = {
if (y.isEmpty) x
else if (x.isEmpty) y
else new Attributes(x.m ++ y.m)
}
}
}
127 changes: 127 additions & 0 deletions sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* 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

import cats.Show
import cats.implicits.catsSyntaxEitherId
import cats.implicits.catsSyntaxOptionId
import cats.implicits.catsSyntaxSemigroup
import cats.implicits.showInterpolator
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.sdk.BuildInfo
import org.typelevel.otel4s.sdk.Resource.ResourceInitiationError
import org.typelevel.otel4s.semconv.resource.attributes.ResourceAttributes._

import scala.util.control.NoStackTrace

/** [[Resource]] serves as a representation of a resource that captures
* essential identifying information regarding the entities associated with
* reported signals, such as statistics or traces.
* @param attributes
* \- a collection of [[Attribute]]s
* @param schemaUrl
* \- an optional schema URL
*/
final case class Resource(
attributes: Attributes,
schemaUrl: Option[String]
) {

/** Merges [[Resource]] into another [[Resource]]. If the same attribute
* exists in both resources, the attribute in the other [[Resource]] will be
* used.
* @param other
* \- the other [[Resource]] to merge into.
* @return
* a new [[Resource]] with the merged attributes.
*/
def mergeInto(other: Resource): Either[ResourceInitiationError, Resource] = {
if (other == Resource.Empty) this.asRight
else {
val schemaUrlOptEither = (other.schemaUrl, schemaUrl) match {
case (Some(otherUrl), Some(url)) =>
if (otherUrl == url)
url.some.asRight
else
ResourceInitiationError.SchemaUrlConflict.asLeft
case (otherUrl, url) =>
otherUrl.orElse(url).asRight
}

schemaUrlOptEither.map(
Resource(
other.attributes |+| attributes,
_
)
)
}
}

/** Unsafe version of [[Resource.mergeInto]] which trows an exception if the
* merge fails.
*/
def mergeIntoUnsafe(other: Resource): Resource =
mergeInto(other).fold(
throw _,
identity
)
}

object Resource {
sealed trait ResourceInitiationError extends NoStackTrace
object ResourceInitiationError {
case object SchemaUrlConflict extends ResourceInitiationError
}

def apply(attributes: Attributes): Resource =
Resource(attributes, None)

/** Returns an empty [[Resource]]. It is strongly recommended to start with
* [[Resource.Default]] instead of this method to include SDK required
* attributes.
*
* @return
* an empty <pre>Resource</pre>.
*/
val Empty: Resource = Resource(Attributes.Empty)

private val TelemetrySdk: Resource = Resource(
Attributes(
Attribute(TelemetrySdkName, "otel4s"),
Attribute(TelemetrySdkLanguage, TelemetrySdkLanguageValue.Scala.value),
Attribute(TelemetrySdkVersion, BuildInfo.version)
)
)

private val Mandatory: Resource = Resource(
Attributes(
Attribute(ServiceName, "unknown_service:scala")
)
)

/** Returns the default [[Resource]]. This resource contains the default
* attributes provided by the SDK.
*
* @return
* a <pre>Resource</pre>.
*/
val Default: Resource = TelemetrySdk.mergeIntoUnsafe(Mandatory)

implicit val showResource: Show[Resource] =
r => show"Resource(${r.attributes}, ${r.schemaUrl})"

}
Loading