aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Topolnjak <ivantopo@gmail.com>2019-02-24 18:49:59 +0100
committerIvan Topolnjak <ivantopo@gmail.com>2019-03-27 13:56:30 +0100
commitff7c7b89335e3d3c463a57cd24321a1419a587ed (patch)
tree0ba8deb0b653e92d39a48667aaccdd0d5ee8e81b
parent8e177b66d99c166244e1d831d41b14f54f6e73d3 (diff)
downloadKamon-ff7c7b89335e3d3c463a57cd24321a1419a587ed.tar.gz
Kamon-ff7c7b89335e3d3c463a57cd24321a1419a587ed.tar.bz2
Kamon-ff7c7b89335e3d3c463a57cd24321a1419a587ed.zip
implement a generic abstraction for handling Tags
-rw-r--r--kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala180
-rw-r--r--kamon-core/src/main/scala/kamon/tag/Lookups.scala149
-rw-r--r--kamon-core/src/main/scala/kamon/tag/Tags.scala346
3 files changed, 675 insertions, 0 deletions
diff --git a/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala b/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala
new file mode 100644
index 00000000..1a83e1c9
--- /dev/null
+++ b/kamon-core-tests/src/test/scala/kamon/tag/TagsSpec.scala
@@ -0,0 +1,180 @@
+package kamon.tag
+
+import java.util.Optional
+
+import org.scalatest.{Matchers, WordSpec}
+
+import scala.collection.JavaConverters.mapAsJavaMapConverter
+
+class TagsSpec extends WordSpec with Matchers {
+ import Lookups._
+
+ "Tags" should {
+ "silently drop null and unacceptable keys and/or values when constructed from the companion object builders" in {
+ Tags.from(NullString, NullString).all().size shouldBe 0
+ Tags.from(EmptyString, NullString).all().size shouldBe 0
+ Tags.from(EmptyString, "value").all().size shouldBe 0
+ Tags.from(NullString, "value").all().size shouldBe 0
+ Tags.from("key", NullString).all().size shouldBe 0
+ Tags.from("key", NullBoolean).all().size shouldBe 0
+ Tags.from("key", NullLong).all().size shouldBe 0
+
+ Tags.from(BadScalaTagMap).all().size shouldBe 0
+ Tags.from(BadJavaTagMap).all().size shouldBe 0
+ }
+
+ "silently drop null keys and/or values when created with the .withTag, withTags or .and methods" in {
+ val tags = Tags.from("initialKey", "initialValue")
+ .withTag(NullString, NullString)
+ .withTag(EmptyString, NullString)
+ .withTag(EmptyString, "value")
+ .withTag(NullString, "value")
+ .withTag("key", NullString)
+ .withTag("key", NullBoolean)
+ .withTag("key", NullLong)
+ .and(NullString, NullString)
+ .and(EmptyString, NullString)
+ .and(EmptyString, "value")
+ .and(NullString, "value")
+ .and("key", NullString)
+ .and("key", NullBoolean)
+ .and("key", NullLong)
+
+ tags.all().length shouldBe 1
+ tags.all().head.asInstanceOf[Tag.String].key shouldBe "initialKey"
+ tags.all().head.asInstanceOf[Tag.String].value shouldBe "initialValue"
+ }
+
+ "create a properly populated instance when valid pairs are provided" in {
+ Tags.from("isAwesome", true).all().size shouldBe 1
+ Tags.from("name", "kamon").all().size shouldBe 1
+ Tags.from("age", 5L).all().size shouldBe 1
+
+ Tags.from(GoodScalaTagMap).all().size shouldBe 3
+ Tags.from(GoodJavaTagMap).all().size shouldBe 3
+
+ Tags.from("initial", "initial")
+ .withTag("isAwesome", true)
+ .withTag("name", "Kamon")
+ .withTag("age", 5L)
+ .and("isAvailable", true)
+ .and("website", "kamon.io")
+ .and("supportedPlatforms", 1L)
+ .all().size shouldBe 7
+ }
+
+ "override pre-existent tags when merging with other Tags instance" in {
+ val leftTags = Tags.from(GoodScalaTagMap)
+ val rightTags = Tags
+ .from("name", "New Kamon")
+ .and("age", 42L)
+ .and("isAwesome", false) // just for testing :)
+
+ val tags = leftTags.withTags(rightTags)
+ tags.get(plain("name")) shouldBe "New Kamon"
+ tags.get(plainLong("age")) shouldBe 42L
+ tags.get(plainBoolean("isAwesome")) shouldBe false
+
+ val andTags = tags and leftTags
+ andTags.get(plain("name")) shouldBe "Kamon"
+ andTags.get(plainLong("age")) shouldBe 5L
+ andTags.get(plainBoolean("isAwesome")) shouldBe true
+ }
+
+ "provide typed access to the contained pairs when looking up values" in {
+ val tags = Tags.from(GoodScalaTagMap)
+
+ tags.get(plain("name")) shouldBe "Kamon"
+ tags.get(plain("none")) shouldBe null
+ tags.get(option("name")) shouldBe Option("Kamon")
+ tags.get(option("none")) shouldBe None
+ tags.get(optional("name")) shouldBe Optional.of("Kamon")
+ tags.get(optional("none")) shouldBe Optional.empty()
+
+ tags.get(plainLong("age")) shouldBe 5L
+ tags.get(plainLong("nil")) shouldBe null
+ tags.get(longOption("age")) shouldBe Option(5L)
+ tags.get(longOption("nil")) shouldBe None
+ tags.get(longOptional("age")) shouldBe Optional.of(5L)
+ tags.get(longOptional("nil")) shouldBe Optional.empty()
+
+ tags.get(plainBoolean("isAwesome")) shouldBe true
+ tags.get(plainBoolean("isUnknown")) shouldBe null
+ tags.get(booleanOption("isAwesome")) shouldBe Some(true)
+ tags.get(booleanOption("isUnknown")) shouldBe None
+ tags.get(booleanOptional("isAwesome")) shouldBe Optional.of(true)
+ tags.get(booleanOptional("isUnknown")) shouldBe Optional.empty()
+
+ tags.get(coerce("age")) shouldBe "5"
+ tags.get(coerce("isAwesome")) shouldBe "true"
+ tags.get(coerce("unknown")) shouldBe "unknown"
+ }
+
+ "allow iterating over all contained tags" in {
+ val tags = Tags.from(Map(
+ "age" -> 5L,
+ "name" -> "Kamon",
+ "isAwesome" -> true,
+ "hasTracing" -> true,
+ "website" -> "kamon.io",
+ "luckyNumber" -> 7L
+ ))
+
+ tags.iterator().length shouldBe 6
+ tags.iterator().find(matchPair("age", 5L)) shouldBe defined
+ tags.iterator().find(matchPair("luckyNumber", 7L)) shouldBe defined
+ tags.iterator().find(matchPair("hasTracing", true)) shouldBe defined
+ tags.iterator().find(matchPair("isAwesome", true)) shouldBe defined
+ tags.iterator().find(matchPair("website", "kamon.io")) shouldBe defined
+ tags.iterator().find(matchPair("name", "Kamon")) shouldBe defined
+ }
+
+ "be equal to other Tags instance with the same tags" in {
+ Tags.from(GoodScalaTagMap) shouldBe Tags.from(GoodScalaTagMap)
+ Tags.from(GoodJavaTagMap) shouldBe Tags.from(GoodJavaTagMap)
+ }
+
+ "have a readable toString implementation" in {
+ Tags.from(GoodScalaTagMap).toString() should include("age=5")
+ Tags.from(GoodScalaTagMap).toString() should include("name=Kamon")
+ Tags.from(GoodScalaTagMap).toString() should include("isAwesome=true")
+ }
+ }
+
+ def matchPair(key: String, value: Any) = { tag: Tag => {
+ tag match {
+ case t: Tag.String => t.key == key && t.value == value
+ case t: Tag.Long => t.key == key && t.value == value
+ case t: Tag.Boolean => t.key == key && t.value == value
+ }
+
+ }}
+
+
+ val NullString: java.lang.String = null
+ val NullBoolean: java.lang.Boolean = NullString.asInstanceOf[java.lang.Boolean]
+ val NullLong: java.lang.Long = null
+ val EmptyString: java.lang.String = ""
+
+ val GoodScalaTagMap: Map[String, Any] = Map(
+ "age" -> 5L,
+ "name" -> "Kamon",
+ "isAwesome" -> true
+ )
+
+ val BadScalaTagMap: Map[String, Any] = Map(
+ NullString -> NullString,
+ EmptyString -> NullString,
+ NullString -> NullString,
+ EmptyString -> NullString,
+ EmptyString -> "value",
+ NullString -> "value",
+ "key" -> NullString,
+ "key" -> NullBoolean,
+ "key" -> NullLong
+ )
+
+ val GoodJavaTagMap = GoodScalaTagMap.asJava
+ val BadJavaTagMap = BadScalaTagMap.asJava
+
+}
diff --git a/kamon-core/src/main/scala/kamon/tag/Lookups.scala b/kamon-core/src/main/scala/kamon/tag/Lookups.scala
new file mode 100644
index 00000000..44ffb6f4
--- /dev/null
+++ b/kamon-core/src/main/scala/kamon/tag/Lookups.scala
@@ -0,0 +1,149 @@
+package kamon.tag
+
+import java.util.Optional
+import java.lang.{Boolean => JBoolean, Long => JLong, String => JString}
+
+import kamon.tag.Tags.Lookup
+
+import scala.reflect.ClassTag
+
+object Lookups {
+
+ /**
+ * Finds a String value associated to the provided key and returns it. If the key is not present or the value
+ * associated with they is not a String then a null is returned.
+ */
+ def plain(key: JString) = new Lookup[JString] {
+ override def run(storage: Map[JString, Any]): JString =
+ findAndTransform(key, storage, _plainString, null)
+ }
+
+
+ /**
+ * Finds a String value associated to the provided key and returns it, wrapped in an Option[String]. If the key is
+ * not present or the value associated with they is not a String then a None is returned.
+ */
+ def option(key: JString) = new Lookup[Option[JString]] {
+ override def run(storage: Map[JString, Any]): Option[JString] =
+ findAndTransform(key, storage, _stringOption, None)
+ }
+
+
+ /**
+ * Finds a String value associated to the provided key and returns it, wrapped in an Optional[String]. If the key
+ * is not present or the value associated with they is not a String then Optional.empty() is returned.
+ */
+ def optional(key: JString) = new Lookup[Optional[String]] {
+ override def run(storage: Map[String, Any]): Optional[String] =
+ findAndTransform(key, storage, _stringOptional, Optional.empty())
+ }
+
+
+ /**
+ * Finds the value associated to the provided key and coerces it to a String representation. If the key is not
+ * present then "unknown" (as a String) will be returned. If the value associated with the key is not a String then
+ * the value of the key will be transformed into a String and returned.
+ *
+ * This lookup type is guaranteed to return a non-null String representation of value.
+ */
+ def coerce(key: String) = new Lookup[String] {
+ override def run(storage: Map[String, Any]): String = {
+ val value = storage(key)
+ if(value == null)
+ "unknown"
+ else
+ value.toString
+ }
+ }
+
+
+ /**
+ * Finds a Boolean value associated to the provided key and returns it. If the key is not present or the value
+ * associated with they is not a Boolean then a null is returned.
+ */
+ def plainBoolean(key: String) = new Lookup[JBoolean] {
+ override def run(storage: Map[String, Any]): JBoolean =
+ findAndTransform(key, storage, _plainBoolean, null)
+ }
+
+
+ /**
+ * Finds a Boolean value associated to the provided key and returns it, wrapped in an Option[Boolean]. If the key
+ * is not present or the value associated with they is not a Boolean then a None is returned.
+ */
+ def booleanOption(key: String) = new Lookup[Option[JBoolean]] {
+ override def run(storage: Map[String, Any]): Option[JBoolean] =
+ findAndTransform(key, storage, _booleanOption, None)
+ }
+
+
+ /**
+ * Finds a Boolean value associated to the provided key and returns it, wrapped in an Optional[Boolean]. If the key
+ * is not present or the value associated with they is not a Boolean then Optional.empty() is returned.
+ */
+ def booleanOptional(key: String) = new Lookup[Optional[JBoolean]] {
+ override def run(storage: Map[String, Any]): Optional[JBoolean] =
+ findAndTransform(key, storage, _booleanOptional, Optional.empty())
+ }
+
+
+ /**
+ * Finds a Long value associated to the provided key and returns it. If the key is not present or the value
+ * associated with they is not a Long then a null is returned.
+ */
+ def plainLong(key: String) = new Lookup[JLong] {
+ override def run(storage: Map[String, Any]): JLong =
+ findAndTransform(key, storage, _plainLong, null)
+ }
+
+
+ /**
+ * Finds a Long value associated to the provided key and returns it, wrapped in an Option[Long]. If the key is
+ * not present or the value associated with they is not a Long then a None is returned.
+ */
+ def longOption(key: String) = new Lookup[Option[JLong]] {
+ override def run(storage: Map[String, Any]): Option[JLong] =
+ findAndTransform(key, storage, _longOption, None)
+ }
+
+
+ /**
+ * Finds a Long value associated to the provided key and returns it, wrapped in an Optional[Long]. If the key
+ * is not present or the value associated with they is not a Long then Optional.empty() is returned.
+ */
+ def longOptional(key: String) = new Lookup[Optional[JLong]] {
+ override def run(storage: Map[String, Any]): Optional[JLong] =
+ findAndTransform(key, storage, _longOptional, Optional.empty())
+ }
+
+
+ ////////////////////////////////////////////////////////////////
+ // Transformation helpers for the lookup DSL //
+ ////////////////////////////////////////////////////////////////
+
+ private def findAndTransform[T, R](key: String, storage: Map[String, Any], transform: R => T, default: T)
+ (implicit ct: ClassTag[R]): T = {
+
+ // This assumes that this code will only be used to lookup values from a Tags instance
+ // for which the underlying map always has "null" as the default value.
+ val value = storage(key)
+
+ if(value == null || !ct.runtimeClass.isInstance(value))
+ default
+ else
+ transform(value.asInstanceOf[R])
+ }
+
+ private val _plainString = (a: JString) => a
+ private val _stringOption = (a: JString) => Option(a)
+ private val _stringOptional = (a: JString) => Optional.of(a)
+
+ private val _plainLong = (a: JLong) => a
+ private val _longOption = (a: JLong) => Option(a)
+ private val _longOptional = (a: JLong) => Optional.of(a)
+
+ private val _plainBoolean = (a: JBoolean) => a
+ private val _booleanOption = (a: JBoolean) => Option(a)
+ private val _booleanOptional = (a: JBoolean) => Optional.of(a)
+
+}
diff --git a/kamon-core/src/main/scala/kamon/tag/Tags.scala b/kamon-core/src/main/scala/kamon/tag/Tags.scala
new file mode 100644
index 00000000..b7813da6
--- /dev/null
+++ b/kamon-core/src/main/scala/kamon/tag/Tags.scala
@@ -0,0 +1,346 @@
+package kamon.tag
+
+import kamon.tag.Tags.Lookup
+
+import scala.collection.JavaConverters.asScalaIteratorConverter
+import java.lang.{Boolean => JBoolean, Long => JLong, String => JString}
+
+import org.slf4j.LoggerFactory
+
+
+/**
+ * Marker trait for allowed Tag implementations. Users are not meant to create implementations of this trait outside
+ * of Kamon.
+ */
+sealed trait Tag
+
+object Tag {
+
+ /**
+ * Represents a String key pointing to a String value.
+ */
+ trait String extends Tag {
+ def key: JString
+ def value: JString
+ }
+
+ /**
+ * Represents a String key pointing to a Boolean value.
+ */
+ trait Boolean extends Tag {
+ def key: JString
+ def value: JBoolean
+ }
+
+ /**
+ * Represents a String key pointing to a Long value.
+ */
+ trait Long extends Tag {
+ def key: JString
+ def value: JLong
+ }
+}
+
+
+/**
+ * A immutable collection of key/value pairs with specialized support for storing String keys pointing to String, Long
+ * and/or Boolean values.
+ *
+ * Instances of Tags store all pairs in the same data structure, but preserving type information for the stored pairs
+ * and providing a simple DSL for accessing those values and expressing type expectations. It is also possible to
+ * lookup pairs without prescribing a mechanism for handling missing values. I.e. users of this class can decide
+ * whether to receive a null, java.util.Optional, scala.Option or any other value when looking up a pair.
+ *
+ * Tags can only be created from the builder functions on the Tags companion object. There are two different options
+ * to read the contained pairs from a Tags instance:
+ *
+ * 1. Using the lookup DSL. You can use the Lookup DSL when you know exactly that you are trying to get out of the
+ * tags instance. The lookup DSL is biased towards String keys since they are by far the most common case. For
+ * example, to get a given tag as an Option[String] and another as an Option[Boolean] the following code should
+ * suffice:
+ *
+ * import kamon.tag.Tags.Lookup._
+ * val tags = Tags.from(tagMap)
+ * val name = tags.get(option("name"))
+ * val isSignedIn = tags.get(booleanOption("isSignedIn"))
+ *
+ * 2. Using the .all() and .iterator variants. This option requires you to test the returned instances to verify
+ * whether they are a Tag.String, Tag.Long or Tag.Boolean instance and act accordingly. Fortunately this
+ * cumbersome operation is rarely necessary on user-facing code.
+ *
+ */
+class Tags private(private val _tags: Map[String, Any]) {
+ import Tags.withPair
+
+ /**
+ * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated
+ * with another value then the previous value will be discarded and overwritten with the provided one.
+ */
+ def withTag(key: String, value: JString): Tags =
+ withPair(this, key, value)
+
+
+ /**
+ * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated
+ * with another value then the previous value will be discarded and overwritten with the provided one.
+ */
+ def withTag(key: String, value: JBoolean): Tags =
+ withPair(this, key, value)
+
+
+ /**
+ * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated
+ * with another value then the previous value will be discarded and overwritten with the provided one.
+ */
+ def withTag(key: String, value: JLong): Tags =
+ withPair(this, key, value)
+
+
+ /**
+ * Creates a new Tags instance that includes all the tags from the provided Tags instance. If any of the tags in this
+ * instance are associated to a key present on the provided instance then the previous value will be discarded and
+ * overwritten with the provided one.
+ */
+ def withTags(other: Tags): Tags =
+ new Tags(_tags ++ other._tags)
+
+
+ /**
+ * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated
+ * with another value then the previous value will be discarded and overwritten with the provided one.
+ */
+ def and(key: String, value: JString): Tags =
+ withPair(this, key, value)
+
+
+ /**
+ * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated
+ * with another value then the previous value will be discarded and overwritten with the provided one.
+ */
+ def and(key: String, value: JBoolean): Tags =
+ withPair(this, key, value)
+
+
+ /**
+ * Creates a new Tags instance that includes the provided key/value pair. If the provided key was already associated
+ * with another value then the previous value will be discarded and overwritten with the provided one.
+ */
+ def and(key: String, value: JLong): Tags =
+ withPair(this, key, value)
+
+
+ /**
+ * Creates a new Tags instance that includes all the tags from the provided Tags instance. If any of the tags in this
+ * instance are associated to a key present on the provided instance then the previous value will be discarded and
+ * overwritten with the provided one.
+ */
+ def and(other: Tags): Tags =
+ new Tags(_tags ++ other._tags)
+
+ /**
+ * Executes a tag lookup on the instance. The return type of this function will depend on the provided Lookup
+ * instance. Take a look at the built-in lookups on the Tags.Lookup companion object for more information.
+ */
+ def get[T](lookup: Lookup[T]): T =
+ lookup.run(_tags)
+
+ /**
+ * Returns a immutable sequence of tags created from the contained tags internal representation. Calling this method
+ * will cause the creation of a new data structure. Unless you really need to have all the tags as immutable
+ * instances it is recommended to use the .iterator() function instead.
+ *
+ * The returned sequence contains immutable values and is safe to share across threads.
+ */
+ def all(): Seq[Tag] =
+ _tags.foldLeft(List.empty[Tag]) {
+ case (ts, (key, value)) => value match {
+ case v: String => new immutable.String(key, v) :: ts
+ case v: Boolean => new immutable.Boolean(key, v) :: ts
+ case v: Long => new immutable.Long(key, v) :: ts
+ }
+ }
+
+ /**
+ * Returns an iterator of tags. The underlying iterator reuses the Tag instances to avoid unnecessary intermediate
+ * allocations and thus, it is not safe to share across threads. The most common case for tags iterators is on
+ * reporters which will need to iterate through all existent tags only to copy their values into a separate data
+ * structure that will be sent to the external systems.
+ */
+ def iterator(): Iterator[Tag] = new Iterator[Tag] {
+ private val _entriesIterator = _tags.iterator
+ private var _longTag: mutable.Long = null
+ private var _stringTag: mutable.String = null
+ private var _booleanTag: mutable.Boolean = null
+
+ override def hasNext: Boolean =
+ _entriesIterator.hasNext
+
+ override def next(): Tag = {
+ val (key, value) = _entriesIterator.next()
+ value match {
+ case v: String => stringTag(key, v)
+ case v: Boolean => booleanTag(key, v)
+ case v: Long => longTag(key, v)
+ }
+ }
+
+ private def stringTag(key: JString, value: JString): Tag.String =
+ if(_stringTag == null) {
+ _stringTag = new mutable.String(key, value)
+ _stringTag
+ } else _stringTag.updated(key, value)
+
+ private def booleanTag(key: JString, value: JBoolean): Tag.Boolean =
+ if(_booleanTag == null) {
+ _booleanTag = new mutable.Boolean(key, value)
+ _booleanTag
+ } else _booleanTag.updated(key, value)
+
+ private def longTag(key: JString, value: JLong): Tag.Long =
+ if(_longTag == null) {
+ _longTag = new mutable.Long(key, value)
+ _longTag
+ } else _longTag.updated(key, value)
+ }
+
+
+ override def equals(other: Any): Boolean =
+ other != null && other.isInstanceOf[Tags] && other.asInstanceOf[Tags]._tags == this._tags
+
+
+ override def toString: JString = {
+ val sb = new StringBuilder()
+ sb.append("Tags{")
+
+ var hasTags = false
+ _tags.foreach { case (k, v) =>
+ if(hasTags)
+ sb.append(",")
+
+ sb.append(k)
+ .append("=")
+ .append(v)
+
+ hasTags = true
+ }
+
+ sb.append("}").toString()
+ }
+
+ private object immutable {
+ class String(val key: JString, val value: JString) extends Tag.String
+ class Boolean(val key: JString, val value: JBoolean) extends Tag.Boolean
+ class Long(val key: JString, val value: JLong) extends Tag.Long
+ }
+
+ private object mutable {
+ class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString]
+ class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean]
+ class Long(var key: JString, var value: JLong) extends Tag.Long with Updateable[JLong]
+
+ trait Updateable[T] {
+ var key: JString
+ var value: T
+
+ def updated(key: JString, value: T): this.type = {
+ this.key = key
+ this.value = value
+ this
+ }
+ }
+ }
+}
+
+object Tags {
+
+ /**
+ * A valid instance of tags that doesn't contain any pairs.
+ */
+ val Empty = new Tags(Map.empty.withDefaultValue(null))
+
+
+ /**
+ * Construct a new Tags instance with a single key/value pair.
+ */
+ def from(key: String, value: JString): Tags =
+ withPair(Empty, key, value)
+
+
+ /**
+ * Construct a new Tags instance with a single key/value pair.
+ */
+ def from(key: String, value: JBoolean): Tags =
+ withPair(Empty, key, value)
+
+
+ /**
+ * Construct a new Tags instance with a single key/value pair.
+ */
+ def from(key: String, value: JLong): Tags =
+ withPair(Empty, key, value)
+
+
+ /**
+ * Constructs a new Tags instance from a Map. The returned Tags will only contain the entries that have String, Long
+ * or Boolean values from the supplied map, any other entry in the map will be ignored.
+ */
+ def from(map: Map[String, Any]): Tags =
+ new Tags(map.filter { case (k, v) => isValidPair(k, v) } withDefaultValue(null))
+
+
+ /**
+ * Constructs a new Tags instance from a Map. The returned Tags will only contain the entries that have String, Long
+ * or Boolean values from the supplied map, any other entry in the map will be ignored.
+ */
+ def from(map: java.util.Map[String, Any]): Tags = {
+ val allowedTags = Map.newBuilder[String, Any]
+ map.entrySet()
+ .iterator()
+ .asScala
+ .foreach(e => if(isValidPair(e.getKey, e.getValue)) allowedTags += (e.getKey -> e.getValue))
+
+ new Tags(allowedTags.result().withDefaultValue(null))
+ }
+
+
+ private val _logger = LoggerFactory.getLogger(classOf[Tags])
+
+ private def withPair(parent: Tags, key: String, value: Any): Tags =
+ if(isValidPair(key, value))
+ new Tags(parent._tags.updated(key, value))
+ else
+ parent
+
+ private def isValidPair(key: String, value: Any): Boolean = {
+ val isValidKey = key != null && key.nonEmpty
+ val isValidValue = isAllowedTagValue(value)
+ val isValid = isValidKey && isValidValue
+
+ if(!isValid && _logger.isDebugEnabled) {
+ if(!isValidKey && !isValidValue)
+ _logger.debug(s"Dismissing tag with invalid key [$key] and invalid value [$value]")
+ else if(!isValidKey)
+ _logger.debug(s"Dismissing tag with invalid key [$key] and value [$value]")
+ else
+ _logger.debug(s"Dismissing tag with key [$key] and invalid value [$value]")
+ }
+
+ isValid
+ }
+
+ private def isAllowedTagValue(v: Any): Boolean =
+ v != null && (v.isInstanceOf[String] || v.isInstanceOf[Boolean] || v.isInstanceOf[Long])
+
+
+ /**
+ * Describes a strategy to lookup values from a Tags instance. Implementations of this interface will be provided
+ * with the actual data structure containing the tags and must perform any necessary runtime type checks to ensure
+ * that the returned value is in assignable to the expected type T.
+ *
+ * Several implementation are provided in the Lookup companion object and it is recommended to import and use those
+ * definitions when looking up keys from a Tags instance.
+ */
+ trait Lookup[T] {
+ def run(storage: Map[String, Any]): T
+ }
+} \ No newline at end of file