aboutsummaryrefslogblamecommitdiff
path: root/kamon-core/src/main/scala/kamon/tag/TagSet.scala
blob: c304a9dfb3b072377803790fd5b492fb48cb24bf (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                 
                              
 
                                                                        
                                    
 
                                                          

                              








                                                                                                                       

                                                                                                                 















                                                                                                                     
                                                                        
                        
 
 
     
                                                                                                                         

                                                                                                         
                                                    



                              
                                                                                                                         

                                                                                                         
                                                     



                              
                                                                                                                         

                                                                                                         
                                                  



                              
                                                                                                                          


                                                                                                                     
                                       
              


     
                                                                                                                         

                                                                                                         
                                                



                              
                                                                                                                         

                                                                                                         
                                                 



                              
                                                                                                                         

                                                                                                         
                                              



                              
                                                                                                                          


                                                                                                                     





                                                                                                   
 

     


                                                                     
                       





                                                             
                        




                                                                                                                    

                                    
                            
 







                                                                                                                       







                                                                              
       



        
 
 






                                                                                                                     



                                                                         




                                   




                                                     




                                                                     
                                                          




                                                                        
                                                            




                                                               
                                                      



                                         
                                            
                                                                                                        
 

                                





                                    


                                                         


                      
                            
                    
                            






                             



                                                                   
   
 
 
               
 
























































                                                                                                                        
 
   
 



                                                              
























                                                                   


     
                                                                   
      
                                                 



                               
                                                                   
      
                                                  



                               
                                                                   
      
                                               



                               

                                                                                                                      
      





                                                                                             


     

                                                                                                                      
      
                                                       




                                                              
 
                          


   
                                                                
 
                                                                         





                                                                                





















                                                                                            




                                                                         
 














                                                                                                           

   
package kamon.tag

import kamon.tag.TagSet.Lookup

import java.lang.{Boolean => JBoolean, Long => JLong, String => JString}
import java.util.function.BiConsumer

import org.eclipse.collections.impl.map.mutable.UnifiedMap
import org.slf4j.LoggerFactory

/**
  * 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.
  *
  * TagSet instances can only be created from the builder functions on the TagSet 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 TagSet private(private val _underlying: UnifiedMap[String, Any]) {
  import TagSet.withPair


  /**
    * Creates a new TagSet 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): TagSet =
    withPair(this, key, value)


  /**
    * Creates a new TagSet 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): TagSet =
    withPair(this, key, value)


  /**
    * Creates a new TagSet 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): TagSet =
    withPair(this, key, value)


  /**
    * Creates a new TagSet 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: TagSet): TagSet =
    and(other)


  /**
    * Creates a new TagSet 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): TagSet =
    withPair(this, key, value)


  /**
    * Creates a new TagSet 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): TagSet =
    withPair(this, key, value)


  /**
    * Creates a new TagSet 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): TagSet =
    withPair(this, key, value)


  /**
    * Creates a new TagSet 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: TagSet): TagSet = {
    val mergedMap = new UnifiedMap[String, Any](other._underlying.size() + this._underlying.size())
    mergedMap.putAll(this._underlying)
    mergedMap.putAll(other._underlying)
    new TagSet(mergedMap)
  }


  /**
    * Returns whether this TagSet instance does not contain any tags.
    */
  def isEmpty(): Boolean =
    _underlying.isEmpty


  /**
    * Returns whether this TagSet instance contains any tags.
    */
  def nonEmpty(): Boolean =
    !_underlying.isEmpty


  /**
    * Executes a tag lookup. The return type of this function will depend on the provided Lookup. Take a look at the
    * built-in lookups on the [[Lookups]] companion object for more information.
    */
  def get[T](lookup: Lookup[T]): T =
    lookup.execute(_storage)


  /**
    * 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] = {
    var tags: List[Tag] = Nil

    _underlying.forEach(new BiConsumer[String, Any] {
      override def accept(key: String, value: Any): Unit = value match {
        case v: String  => tags = new TagSet.immutable.String(key, v) :: tags
        case v: Boolean => tags = new TagSet.immutable.Boolean(key, v) :: tags
        case v: Long    => tags = new TagSet.immutable.Long(key, v) :: tags
      }
    })

    tags
  }


  /**
    * 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 = _underlying.keyValuesView().iterator()
    private var _longTag: TagSet.mutable.Long = null
    private var _stringTag: TagSet.mutable.String = null
    private var _booleanTag: TagSet.mutable.Boolean = null

    override def hasNext: Boolean =
      _entriesIterator.hasNext

    override def next(): Tag = {
      val pair = _entriesIterator.next()
      pair.getTwo match {
        case v: String  => stringTag(pair.getOne, v)
        case v: Boolean => booleanTag(pair.getOne, v)
        case v: Long    => longTag(pair.getOne, v)
      }
    }

    private def stringTag(key: JString, value: JString): Tag.String =
      if(_stringTag == null) {
        _stringTag = new TagSet.mutable.String(key, value)
        _stringTag
      } else _stringTag.updated(key, value)

    private def booleanTag(key: JString, value: JBoolean): Tag.Boolean =
      if(_booleanTag == null) {
        _booleanTag = new TagSet.mutable.Boolean(key, value)
        _booleanTag
      } else _booleanTag.updated(key, value)

    private def longTag(key: JString, value: JLong): Tag.Long =
      if(_longTag == null) {
        _longTag = new TagSet.mutable.Long(key, value)
        _longTag
      } else _longTag.updated(key, value)
  }

  override def equals(other: Any): Boolean =
    other != null && other.isInstanceOf[TagSet] && other.asInstanceOf[TagSet]._underlying == _underlying

  override def hashCode(): Int =
    _underlying.hashCode()

  override def toString: JString = {
    val sb = new StringBuilder()
    sb.append("Tags{")

    var hasTags = false
    val iterator = _underlying.keyValuesView().iterator()
    while(iterator.hasNext) {
      val pair = iterator.next()
      if(hasTags)
        sb.append(",")

      sb.append(pair.getOne)
        .append("=")
        .append(pair.getTwo)

      hasTags = true
    }

    sb.append("}").toString()
  }

  private val _storage = new TagSet.Storage {
    override def get(key: String): Any = _underlying.get(key)
    override def iterator(): Iterator[Tag] = TagSet.this.iterator()
    override def isEmpty(): Boolean = _underlying.isEmpty
  }
}

object TagSet {

  /**
    * Describes a strategy to lookup values from a TagSet 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] {

    /**
      * Tries to find a value on a TagSet.Storage and returns a representation of it. In some cases the stored object
      * might be returned as-is, in some others it might be transformed or wrapped on Option/Optional to handle missing
      * values. Take a look at the Lookups companion object for examples..
      */
    def execute(storage: TagSet.Storage): T
  }


  /**
    * A temporary structure that accumulates key/value and creates a new TagSet instance from them. It is faster to use
    * a Builder and add tags to it rather than creating TagSet and add each key individually. Builder instances rely on
    * internal mutable state and are not thread safe.
    */
  trait Builder {

    /** Adds a new key/value pair to the builder. */
    def add(key: String, value: Any): Builder

    /** Creates a new TagSet instance that includes all valid key/value pairs added to this builder. */
    def create(): TagSet
  }


  /**
    * Abstracts the actual storage used for a TagSet. This interface resembles a stripped down interface of an immutable
    * map of String to Any, used to expose the underlying structure where tags are stored to Lookups, without leaking
    * the actual implementation.
    */
  trait Storage {

    /**
      * Gets the value associated with the provided key, or null if no value was found. The decision of returning null
      * when the key is not present is a conscious one, backed by the fact that users will never be exposed to this
      * storage interface and they can decide their way of handling missing values by selecting an appropriate lookup.
      */
    def get(key: String): Any

    /**
      * Provides an Iterator that can go through all key/value pairs contained in the Storage instance.
      */
    def iterator(): Iterator[Tag]

    /**
      * Returns true if there are no tags in the storage.
      */
    def isEmpty(): Boolean

  }


  /**
    * A valid instance of tags that doesn't contain any pairs.
    */
  val Empty = new TagSet(UnifiedMap.newMap[String, Any]())


  /**
    * Creates a new Builder instance.
    */
  def builder(): Builder = new Builder {
    private var _tagCount = 0
    private var _tags: List[(String, Any)] = Nil

    override def add(key: String, value: Any): Builder = {
      if(isValidPair(key, value)) {
        _tagCount += 1
        _tags = (key -> value) :: _tags
      }

      this
    }

    override def create(): TagSet = {
      val newMap = new UnifiedMap[String, Any](_tagCount)
      _tags.foreach { case (key, value) => newMap.put(key, value) }
      new TagSet(newMap)
    }
  }


  /**
    * Construct a new TagSet instance with a single key/value pair.
    */
  def from(key: String, value: JString): TagSet =
    withPair(Empty, key, value)


  /**
    * Construct a new TagSet instance with a single key/value pair.
    */
  def from(key: String, value: JBoolean): TagSet =
    withPair(Empty, key, value)


  /**
    * Construct a new TagSet instance with a single key/value pair.
    */
  def from(key: String, value: JLong): TagSet =
    withPair(Empty, key, value)


  /**
    * Constructs a new TagSet instance from a Map. The returned TagSet 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]): TagSet = {
    val unifiedMap = new UnifiedMap[String, Any](map.size)
    map.foreach { pair => if(isValidPair(pair._1, pair._2)) unifiedMap.put(pair._1, pair._2)}

    new TagSet(unifiedMap)
  }


  /**
    * Constructs a new TagSet instance from a Map. The returned TagSet 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]): TagSet = {
    val unifiedMap = new UnifiedMap[String, Any](map.size)
    map.forEach(new BiConsumer[String, Any] {
      override def accept(key: String, value: Any): Unit =
        if(isValidPair(key, value)) unifiedMap.put(key, value)
    })

    new TagSet(unifiedMap)
  }


  private val _logger = LoggerFactory.getLogger(classOf[TagSet])

  private def withPair(parent: TagSet, key: String, value: Any): TagSet =
    if(isValidPair(key, value)) {
      val mergedMap = new UnifiedMap[String, Any](parent._underlying.size() + 1)
      mergedMap.putAll(parent._underlying)
      mergedMap.put(key, value)
      new TagSet(mergedMap)
    } 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])

  private object immutable {
    case class String(key: JString, value: JString) extends Tag.String
    case class Boolean(key: JString, value: JBoolean) extends Tag.Boolean
    case class Long(key: JString, value: JLong) extends Tag.Long
  }

  private object mutable {
    case class String(var key: JString, var value: JString) extends Tag.String with Updateable[JString]
    case class Boolean(var key: JString, var value: JBoolean) extends Tag.Boolean with Updateable[JBoolean]
    case 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
      }
    }
  }
}