packages feed

instana-haskell-trace-sdk-0.8.0.0: src/Instana/SDK/Span/SpanData.hs

{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE InstanceSigs        #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-|
Module      : Instana.SDK.Span.SpanData
Description : A type for span data, that is, the content in span.data.
-}
module Instana.SDK.Span.SpanData
  ( SpanData (SpanData)
  , Annotation (..)
  , AnnotationValue (..)
  , empty
  , listAnnotation
  , listValue
  , merge
  , nullAnnotation
  , objectAnnotation
  , optionalAnnotation
  , optionalValue
  , simpleAnnotation
  , simpleValue
  , singleAnnotation
  ) where


import           Data.Aeson                (KeyValue, ToJSON, Value, (.=))
import qualified Data.Aeson                as Aeson
import qualified Data.List                 as L
import qualified Data.Maybe                as Maybe
import           Data.Text                 (Text)
import qualified Data.Vector               as V
import           GHC.Generics

import           Instana.SDK.Internal.Util ((|>))


-- |Represents span.data, that is, the tree of free-form annotatiations for a
-- span.
data SpanData =
  SpanData [Annotation]
  deriving (Eq, Generic, Show)


instance ToJSON SpanData where
  toJSON :: SpanData -> Value
  toJSON (SpanData annotations) =
    Aeson.object $
      map annotationToJson annotations
      |> Maybe.catMaybes


-- |Creates empty span data.
empty :: SpanData
empty = SpanData []


-- |Merges the given annotation into the given span data. If there is a conflict
-- between existing annotations and a new annotation (that is, both are at the
-- same path of keys), this will be resolved as follows:
-- - If both the existing and the new annotation are list annotations, they will
--   be merged. Duplicates are allowed (that is, duplicate values will not be
--   removed).
-- - Otherwise, if the existing annotation or the new annotation is a single
--   annotation, the new annotation will overwrite the existing one.
-- - If both the existing and the new annotation are object annotations, they
--   will be merged by applying these rules recursively.
merge :: Annotation -> SpanData -> SpanData
merge newAnnotation (SpanData existingAnnotations) =
  SpanData $ mergeAnnotation newAnnotation existingAnnotations


mergeAnnotation :: Annotation -> [Annotation] -> [Annotation]
mergeAnnotation newAnnotation existingAnnotations =
  let
    key =
      getKey newAnnotation
    existingAnnotationForKeyMaybe =
      L.find ((== key) . getKey) existingAnnotations
  in
  case existingAnnotationForKeyMaybe of
    Nothing ->
      -- The new key does not exist yet, simply add it.
      existingAnnotations ++ [newAnnotation]
    Just existingAnnotationForKey ->
      let
        annotationsWithoutPrevious =
          (L.delete existingAnnotationForKey existingAnnotations)
      in
      case (existingAnnotationForKey, newAnnotation) of
        (Single _ (List values1), Single _ (List values2)) ->

          -- The key exists already and both the existing and the new annotation
          -- are lists. Merge both list values into one list annotation.
          annotationsWithoutPrevious ++
            [Single key $ List $ values1 ++ values2]

        (Single _ _, _) ->

          -- The key exists already but was previously a single value. We
          -- cannot merge the new annotation into the single value annotation,
          -- overwrite it.
          annotationsWithoutPrevious ++ [newAnnotation]

        (_, Single _ _) ->

          -- The key exists already but the new value is single value. We
          -- cannot merge the new annotation into an existing annotation,
          -- overwrite it.
          annotationsWithoutPrevious ++ [newAnnotation]

        (Object _ previousChildren, Object _ newChildren) ->

          -- The key exists already and both the existing and the new annotation
          -- are object annotations. We merge both structures by recursively
          -- merging all child annotations from the new object annotation with
          -- the child annotations from the existing object annotation.
          let
            mergedChildAnnotations =
              L.foldr
                mergeAnnotation
                previousChildren -- starting value for foldr
                newChildren      -- the list we iterate over
          in
          annotationsWithoutPrevious ++ [Object key mergedChildAnnotations]


-- |Represents a single item in span.data, either an object with child
-- annotations or a single annotation (String, Int, Boolean, List, Maybe etc.)
data Annotation =
  -- |Similar to the type Data.Aeson.Types.Object, an object annotation can hold
  -- multiple child annotations. In
  -- { "http":
  --   { "url": "http://localhost:8080"
  --   , "method": "GET"
  --   , "headers": [("X-Header-1", "value 1), ("X-Header-2", "value 2)]
  --   }
  -- }
  -- the "http" key would be an Object annotation.
  Object Text [Annotation]
  -- |Somewhat similar to Data.Aeson.Types.Pair, a single annotation holds one
  -- single value (String, Number, Boolean, List, Maybe etc.). In the example
  -- above, "url", "method" and headers would be Single annotations.
  | Single Text AnnotationValue
  deriving (Eq, Generic, Show)


-- |Converts a single annotation to a Aeson.KeyValue. An annotation on its own
-- cannot be converted into an Aeson.Value, since it needs to be  wrapped in
-- another Object annotationa and ultimately in SpanData. This implementation
-- will also take care of removing optional annotations that are Nothing.
annotationToJson :: forall kv . KeyValue kv => Annotation -> Maybe kv
annotationToJson annotation =
  case annotation of
    Object key childAnnotations ->
      let
        object :: Value
        object =
          Aeson.object $
            map annotationToJson childAnnotations |> Maybe.catMaybes
        keyValue :: kv
        keyValue = (key .= object)
      in
      Just keyValue

    Single _ (Optional Nothing) ->
      -- drop optional annotations that are Nothing
      Nothing

    Single key annotationValue ->
      let
        pair :: kv
        pair = (key .= (Aeson.toJSON annotationValue))
      in
      Just pair


-- |Retrieves the key from an annotation.
getKey :: Annotation -> Text
getKey (Object key _) = key
getKey (Single key _) = key


-- |Creates a new object annotation. Similar to the type
-- Data.Aeson.Types.Object, an object annotation can hold multiple child
-- annotations. In
-- { "http":
--   { "url": "http://localhost:8080"
--   , "method": "GET"
--   }
-- }
-- the "http" key would be an Object annotation.
objectAnnotation :: Text -> [Annotation] -> Annotation
objectAnnotation key children = Object key children


-- |Creates a single annotation. Somewhat similar to Data.Aeson.Types.Pair, a
-- single annotation holds one value
-- (String, Number, Boolean, List, Maybe etc.).
--
-- The convenience functions simpleAnnotation, listAnnotation, and
-- optionalAnnotation allow creating a single annotation without creating the
-- AnnotationValue explicitly.
singleAnnotation :: Text -> AnnotationValue -> Annotation
singleAnnotation key value = Single key value


-- |Creates a simple annotation, which holds one primitive value (String,
-- Number, Boolean, etc.).
simpleAnnotation :: ToJSON a => Text -> a -> Annotation
simpleAnnotation key = (Single key) . simpleValue


-- |Creates a list annotation, which holds a list of items. For list
-- annotations, consecutive merges with the same key will add to the list
-- instead of overwriting previous values.
listAnnotation :: ToJSON a => Text -> [a] -> Annotation
listAnnotation key = (Single key) . listValue


-- |Creates an optional annotation, which holds a Maybe. If an optional
-- annotation holds a Nothing value, it will be ommitted when SpanData is
-- encoded to JSON.
optionalAnnotation :: ToJSON a => Text -> Maybe a -> Annotation
optionalAnnotation key = (Single key) . optionalValue


-- |A convenience function to create an optional annotation that holds a
-- Nothing.
nullAnnotation :: Text -> Annotation
nullAnnotation key = Single key $ Optional Nothing


-- |Represents the value of a span.data item.
data AnnotationValue =
  Simple Value
  -- |For list annotations consecutive merges with the same key will add to the
  -- list instead of overwriting previous values.
  | List [Value]
  -- |If an annotation is marked as optional, it will be ommitted from the
  -- encoded JSON if it is Nothing.
  | Optional (Maybe Value)
  deriving (Eq, Generic, Show)


instance ToJSON AnnotationValue where
  toJSON :: AnnotationValue -> Value
  toJSON annotation =
    case annotation of
      Simple value ->
        value
      List values ->
        Aeson.Array $ V.fromList values
      Optional maybeValue ->
        case maybeValue of
          Just value -> value
          -- Optional Nothing is actually already dropped in annotationToJson.
          Nothing    -> Aeson.Null


-- |Creates the value part of a simple annotation, that is, a primitive value
-- (String, Number, Boolean, etc.). You might want to use simpleAnnotation
-- directly instead of creating the simple value beforehand.
simpleValue :: ToJSON a => a -> AnnotationValue
simpleValue =
  Simple . Aeson.toJSON


-- |Creates the value part of a list annotation. You might want to use
-- listAnnotation directly instead of creating the list value beforehand.
listValue :: ToJSON a => [a] -> AnnotationValue
listValue =
  List . map Aeson.toJSON


-- |Creates the value part of an optional annotation. You might want to use
-- optionalAnnotation directly instead of creating the optional value
-- beforehand.
optionalValue :: ToJSON a => Maybe a -> AnnotationValue
optionalValue value =
  Optional $ Aeson.toJSON <$> value