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

core-common: add AttributeKey.Make #913

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 65 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 @@ -56,6 +56,55 @@ sealed trait Attribute[A] {

object Attribute {

/** Allows an implicit conversion of `A` to `Attribute`.
*
* @tparam A
* the type of the value
*/
sealed trait Make[A] {
type Key
def make(a: A): Attribute[Key]
}

object Make {
type Aux[A, K] = Make[A] { type Key = K }

/** Creates a [[Make]] with a const name.
*
* @example
* {{{
* case class UserId(id: Int)
* object UserId {
* implicit val userIdFocus: AttributeKey.Focus[UserId, Long] =
* _.id.toLong
* implicit val userIdAttribute: Attribute.Make[UserId] =
* Attribute.Make.named("user.id")
* }
*
* val userId = UserId(123)
*
* val attributes = Attributes(userId) // implicitly converted to Attribute[Long]
* }}}
*
* @param name
* the name of the attribute
*
* @tparam A
* the type of the value
*
* @tparam K
* the type of the key
*/
def named[A, K](name: String)(implicit
focus: AttributeKey.Focus[A, K],
select: AttributeKey.KeySelect[K]
): Make.Aux[A, K] =
new Make[A] {
type Key = K
def make(a: A): Attribute[Key] = Attribute(name, a)
}
}

/** Creates an attribute with the given key and value.
*
* @example
Expand All @@ -77,14 +126,28 @@ object Attribute {
* val boolSeqAttribute: Attribute[Seq[Boolean]] = Attribute("key", Seq(false))
* }}}
*
* @example
* a projected attribute type:
* {{{
* case class UserId(id: Int)
* implicit val userIdKeySelect: AttributeKey.Focus[UserId, Long] = _.id.toLong
* val attribute: Attribute[Long] = Attribute("key", UserId(1))
* }}}
*
* @param name
* the key name of an attribute
*
* @param value
* the value of an attribute
*/
def apply[A: AttributeKey.KeySelect](name: String, value: A): Attribute[A] =
Impl(AttributeKey.KeySelect[A].make(name), value)
def apply[A, Key](name: String, value: A)(implicit
focus: AttributeKey.Focus[A, Key],
select: AttributeKey.KeySelect[Key]
): Attribute[Key] =
AttributeKey.KeySelect[Key].make(name).apply(value)

implicit def materialize[A](value: A)(implicit ev: Make[A]): Attribute[ev.Key] =
ev.make(value)
Comment on lines +149 to +150
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The implicit conversion happens here.


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

Expand Down
76 changes: 74 additions & 2 deletions core/common/src/main/scala/org/typelevel/otel4s/AttributeKey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,62 @@ import cats.syntax.show._
* the type of value that can be set with the key
*/
sealed trait AttributeKey[A] {
import AttributeKey.Focus

/** The name of the attribute key. */
def name: String

/** The type of the attribute value. */
def `type`: AttributeType[A]

/** @return
* an [[`Attribute`]] associating this key with the given value
*/
final def apply(value: A): Attribute[A] = Attribute(this, value)
final def apply(value: A): Attribute[A] =
Attribute(this, value)

/** Creates an [[Attribute]] with the focused value.
*
* @example
* {{{
* case class UserId(id: Int)
* implicit val userIdFocus: AttributeKey.Focus[UserId, Long] = _.id.toLong
*
* val userIdKey = AttributeKey[Long]("user.id")
*
* val attribute: Attribute[Long] = userIdKey(UserId(1))
* val attribute: Attribute[Long] = Attribute("key", UserId(1))
* }}}
*
* @return
* an [[`Attribute`]] associating this key with the given value
*/
final def apply[In](value: In)(implicit focus: Focus[In, A]): Attribute[A] =
Attribute(this, focus(value))

/** @return
* an [[`Attribute`]] associating this key with the given value if the value is defined
*/
final def maybe(value: Option[A]): Option[Attribute[A]] = value.map(apply)
final def maybe(value: Option[A]): Option[Attribute[A]] =
value.map(apply)

/** Creates an [[Attribute]] with the focused value if the given option is non-empty.
*
* @example
* {{{
* case class UserId(id: Int)
* implicit val userIdFocus: AttributeKey.Focus[UserId, Long] = _.id.toLong
*
* val userIdKey = AttributeKey[Long]("user.id")
*
* val attribute: Option[Attribute[Long]] = userIdKey.maybe(Some(UserId(1)))
* }}}
*
* @return
* an [[`Attribute`]] associating this key with the given value
*/
final def maybe[In](value: Option[In])(implicit focus: Focus[In, A]): Option[Attribute[A]] =
value.map(apply[In])

/** @return
* an [[`AttributeKey`]] of the same type as this key, with name transformed by `f`
Expand All @@ -64,6 +108,34 @@ object AttributeKey {
}
}

/** Allows creating an attribute from an arbitrary type:
*
* @example
* {{{
* case class UserId(id: Int)
* implicit val userIdFocus: AttributeKey.Focus[UserId, Long] = _.id.toLong
*
* val userIdKey = AttributeKey[Long]("user.id")
*
* val attribute: Attribute[Long] = userIdKey(UserId(1))
* val attribute: Attribute[Long] = Attribute("key", UserId(1))
* }}}
*
* @tparam Value
* the type of the value
*
* @tparam Key
* the type of the key, one of [[AttributeType]]
*/
@annotation.implicitNotFound("Could not find the `AttributeKey.Focus` for value ${Value} and key ${Key}.")
trait Focus[Value, Key] {
def apply(in: Value): Key
}

object Focus {
implicit def id[A]: Focus[A, A] = a => a
}

@annotation.implicitNotFound("""
Could not find the `KeySelect` for ${A}. The `KeySelect` is defined for the following types:
String, Boolean, Long, Double, Seq[String], Seq[Boolean], Seq[Long], Seq[Double].
Expand Down
14 changes: 10 additions & 4 deletions core/common/src/main/scala/org/typelevel/otel4s/Attributes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ sealed trait Attributes
/** Adds an [[`Attribute`]] with the given name and value to these `Attributes`, replacing any `Attribute` with the
* same name and type if one exists.
*/
final def added[T: KeySelect](name: String, value: T): Attributes =
final def added[T, Key](name: String, value: T)(implicit
focus: AttributeKey.Focus[T, Key],
select: KeySelect[Key]
): Attributes =
added(Attribute(name, value))

/** Adds an [[`Attribute`]] with the given key and value to these `Attributes`, replacing any `Attribute` with the
Expand Down Expand Up @@ -204,9 +207,12 @@ object Attributes extends SpecificIterableFactory[Attribute[_], Attributes] {
* @param value
* the value of the attribute
*/
def addOne[A: KeySelect](name: String, value: A): this.type = {
val key = KeySelect[A].make(name)
builder.addOne(key.name -> Attribute(key, value))
def addOne[A, Key](name: String, value: A)(implicit
focus: AttributeKey.Focus[A, Key],
select: KeySelect[Key]
): this.type = {
val key = KeySelect[Key].make(name)
builder.addOne(key.name -> key(value))
this
}

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

import munit.FunSuite

class AttributeSuite extends FunSuite {

private case class UserId(id: String)
private val userIdKey: AttributeKey[String] = AttributeKey("user.id")
private implicit val userIdFocus: AttributeKey.Focus[UserId, String] = _.id

test("use implicit Focus to derive a type of an attribute") {
val stringAttribute = Attribute("user.id", "123")
val liftedAttribute = Attribute("user.id", UserId("123"))

assertEquals(stringAttribute, liftedAttribute)
}

test("use implicit Focus to add an attribute to a builder") {
val builder = Attributes.newBuilder

builder += userIdKey(UserId("1"))
builder ++= userIdKey.maybe(Some(UserId("2")))
builder.addOne("user.id", UserId("3"))

val expected = Attributes(
Attribute("user.id", "1"),
Attribute("user.id", "2"),
Attribute("user.id", "3")
)

assertEquals(builder.result(), expected)
}

test("use implicit Focus to add an attribute to attributes") {
val attributes = Attributes.empty.added("user.id", UserId("1"))

assertEquals(attributes.get[String]("user.id").map(_.value), Some("1"))
}

test("implicitly materialize an attribute using Attribute.Make") {
case class AssetId(id: Int)

implicit val assetIdFocus: AttributeKey.Focus[AssetId, Long] =
_.id.toLong

implicit val userIdAttribute: Attribute.Make[UserId] =
Attribute.Make.named("user.id")

implicit val assetIdAttribute: Attribute.Make[AssetId] =
Attribute.Make.named("asset.id")

val attributes = Attributes(UserId("321"), AssetId(123))

assertEquals(attributes.get[String]("user.id").map(_.value), Some("321"))
assertEquals(attributes.get[Long]("asset.id").map(_.value), Some(123L))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class ObservableSuite extends CatsEffectSuite {
.withUnit("unit")
.withDescription("description")
.createWithCallback(
_.record(1234, Attribute[Boolean]("is_false", true))
_.record(1234, Attribute("is_false", true))
)
.use(_ =>
sdk
Expand Down