packages feed

instana-haskell-trace-sdk 0.2.0.0 → 0.3.0.0

raw patch · 32 files changed

+1177/−447 lines, 32 files

Files

CONTRIBUTING.md view
@@ -56,3 +56,16 @@ * `STARTUP_DELAY` (default: `0`): An artificial startup delay in milliseconds, used for integration testing. * `SIMULATE_CONNECTION_LOSS` (default: `false`): Used for integration testing. * `SIMULATE_PID_TRANSLATION` (default: `false`): Used for integration testing.++Publishing a New Release+------------------------++* Make sure ChangeLog.md has an entry for the upcoming release.+* Bump the version in:+    * `README.md` (installation instructions/extra-deps)+    * `instana-haskell-trace-sdk.cabal`+    * `package.yaml`+    * `test/integration/Instana/SDK/IntegrationTest/Metrics.hs` (assertion for `sensorVersion`)+* Commit this change with a commit comment like `chore: version a.b.c.d`+* Build the package with stack and upload it to Hackage.+
ChangeLog.md view
@@ -1,3 +1,35 @@-# Changelog for instana-haskell-sdk+# Changelog for instana-haskell-trace-sdk -## Unreleased changes+## 0.3.0.0+- Honor the environment variable `INSTANA_SERVICE_NAME` to override the default service name in Instana.+- Add a configuration option for overriding the default service name in Instana.+- Add `InstanaSDK.setServiceName` to override the default service name in Instana on a per-call basis.+- Fix: Send correct SDK spans. This is a breaking change. Several functions were renamed or have changed their signature.+In detail:++    - Use `InstanaSDK.addTag` instead of `InstanaSDK.addData` (for SDK+      spans).+    - Use `InstanaSDK.addTagAt` instead of `InstanaSDK.addDataAt` (for SDK spans).+    - For registered spans, replace `InstanaSDK.addData` and `InstanaSDK.addDataAt` with `InstanaSDK.addRegisteredData` and `InstanaSDK.addRegisteredDataAt`. Note that you should probably not create registered spans, but only use SDK spans.+    - Usages like `startEntry "some.span.name"` or `withEntry "some.span.name"` (that is, the span name is passed directly as a literal) will simply continue to work as expected but will require `OverloadedStrings` to be active.+    - Usages where the span name is stored in a `Data.Text` value first and then passed to `startEntry`/`withEntry`/etc. will break. You can fix those by importing+```+import qualified Instana.SDK.Span.SpanType as SpanType+```+and then wrapping the span name in `SpanType.SdkSpan`. For example:+```+spanName = T.pack "some.span.name"+InstanaSDK.withRootEntry instana spanName ...+```+becomes:+```+spanType = SpanType.SdkSpan $ T.pack "some.span.name"+InstanaSDK.withRootEntry instana spanType ...+```++## 0.2.0.0+- Add WAI middleware plug-in to trace HTTP entries automatically.++## 0.1.0.0+- Initial release+
README.md view
@@ -19,7 +19,7 @@  ``` extra-deps:-- instana-haskell-trace-sdk-0.2.0.0+- instana-haskell-trace-sdk-0.3.0.0 ```  Usage@@ -63,6 +63,7 @@       InstanaSDK.defaultConfig         { InstanaSDK.agentHost = Just "127.0.0.1"         , InstanaSDK.agentPort = Just 42699+        , InstanaSDK.serviceName = Just "A Great Hakell Service"         , InstanaSDK.forceTransmissionAfter = Just 1000         , InstanaSDK.forceTransmissionStartingAt = Just 500         , InstanaSDK.maxBufferedSpans = Just 1000@@ -98,6 +99,7 @@       InstanaSDK.defaultConfig         { InstanaSDK.agentHost = Just "127.0.0.1"         , InstanaSDK.agentPort = Just 42699+        , InstanaSDK.serviceName = Just "A Great Hakell Service"         , InstanaSDK.forceTransmissionAfter = Just 1000         , InstanaSDK.forceTransmissionStartingAt = Just 500         , InstanaSDK.maxBufferedSpans = Just 1000@@ -114,7 +116,7 @@  #### Trace HTTP Entries Automatically -You can let the SDK automatically create entry spans for all incoming HTTP requests in a WAI application by using it as a WAI middleware plug-in. Note that exit spans still need to be created manually via the withExit or startExit/stopExit functions (see below).+You can let the SDK automatically create entry spans for all incoming HTTP requests in a WAI application by using it as a WAI middleware plug-in. Note that exit spans still need to be created manually via the `withHttpExit` or `startHttpExit`/`completeExit` functions (see below).  ``` import qualified Instana.Wai.Middleware.Entry as InstanaWaiMiddleware@@ -129,20 +131,32 @@  * `withRootEntry`: Creates an entry span that is the root of a trace (it has no parent span). * `withEntry`: Creates an entry span that has a parent span.-* `withHttpEntry`: A convenience function that examines an HTTP request for Instana tracing headers and creates an entry span.+* `withHttpEntry`: A convenience function that examines an HTTP request for Instana tracing headers and creates an entry span. It will automatically add the correct metadata to the span. You do not need to handle incoming HTTP requests at all when using the Instana WAI middleware plug-in (see above). * `withExit`: Creates an exit span. This can only be called inside a `withRootEntry` or an `withEntry` call, as an exit span needs an entry span as its parent.-* `withHttpExit`: Creates an exit span for a given HTTP client request.+* `withHttpExit`: Creates an exit span for a given HTTP client request. It will automatically add the correct metadata to the span so it should be preferred to `withExit` when tracing outgoing HTTP requests.  #### Low Level API/Explicit Start And Complete  * `startRootEntry`: Starts an entry span that is the beginning of a trace (has no parent span). You will need to call `completeEntry` at some point. * `startEntry`: Starts an entry span. You will need to call `completeEntry` at some point.-* `startHttpEntry`: Starts an entry span for an incoming HTTP request.-* `startExit`: Starts an exit span. You need to call `completeExit`/`completeExitWithData` at some point with the partial exit span value that is returned by this function.-* `startHttpExit`: Starts an exit span for an outgoing HTTP request.+* `startHttpEntry`: Starts an entry span for an incoming HTTP request. It will automatically add the correct metadata to the span. You do not need to handle incoming HTTP requests at all when using the WAI middleware plug-in (see above). You will need to call `completeEntry` at some point.+* `startExit`: Starts an exit span. You will need to call `completeExit` at some point.+* `startHttpExit`: Starts an exit span for an outgoing HTTP request. It will automatically add the correct metadata to the span so it should be preferred to `startExit` when tracing outgoing HTTP requests. You will need to call `completeExit` at some point. * `completeEntry`: Finalizes an entry span. This will put the span into the SDK's span buffer for transmission to the Instana agent. * `completeExit`: Finalizes an exit span. This will put the span into the SDK's span buffer for transmission to the Instana agent. +#### Best Practices++Make sure you have read Instana's [docs on custom tracing](https://docs.instana.io/quick_start/custom_tracing/#tips--best-practices) and in particular the [best practices section](https://docs.instana.io/quick_start/custom_tracing/#tips--best-practices). This documentation contains a lot of useful info for integrating Instana tracing into your code; among other things, it explains which [metadata](https://docs.instana.io/quick_start/custom_tracing/#list-of-processed-tags) can be added to spans (via `InstanaSDK.addTag` and `InstanaSDK.addTagAt`).++Instana differentiates between so-called registered spans and SDK spans. Registered spans are usually created by automatic tracing and there is specialized handling for each registered in Instana's processing pipeline. SDK spans, in contrast, are the type of spans created by using a trace SDK (like the Haskell trace SDK or other, similar SDKs for other runtime platforms). SDK span are processed in a more generic fashion by Instana's processing pipeline.++Note that nearly all spans created with this SDK should be SDK spans. The are only two exceptions, for which this SDK creates registered spans:+- HTTP/WAI entry (server) spans, and+- HTTP exit (client) spans.++The SDK offers special functions to create these registered spans (`withHttpEntry`, `withHttpExit` as well as the corresponding lower level functions `startHttpEntry` and `startHttpExit`, see above).+ ### Configuration Via Environment Variables  Instead of configuring the SDK programatically, as seen above, it can also be configured via environment variables:@@ -150,6 +164,7 @@ * `INSTANA_AGENT_HOST`: The IP or the host of the Instana agent to connect to. Default: 127.0.0.1. * `INSTANA_AGENT_PORT`: The port of the Instana agent to connect to. Default: 42699. * `INSTANA_AGENT_NAME`: When establishing a connection to the Instana agent, the SDK validates the Instana agent's `Server` HTTP response header. Should you have changed the Server name on the agent side, you can use this environment variable to provide the name to match that header against.+* `INSTANA_SERVICE_NAME`: Override the default service name in Instana. * `INSTANA_FORCE_TRANSMISSION_STARTING_AFTER`: Spans are usually buffered before being transmitted to the agent. This setting forces the transmission of all buffered spans after the given amount of milliseconds. Default: 1000. * `INSTANA_FORCE_TRANSMISSION_STARTING_AT`: This setting forces the transmission of all buffered spans when the given number of spans has been buffered. * `INSTANA_MAX_BUFFERED_SPANS`: Limits the number of spans to buffer. When the limit is reached, spans will be dropped. This setting is a safe guard against memory leaks from buffering excessive amounts of spans. It must be larger than `INSTANA_FORCE_TRANSMISSION_STARTING_AT`.
instana-haskell-trace-sdk.cabal view
@@ -1,5 +1,5 @@ name:           instana-haskell-trace-sdk-version:        0.2.0.0+version:        0.3.0.0 synopsis:       SDK for adding custom Instana tracing support to Haskell applications. description:    Please also see the README on Github at <https://github.com/instana/haskell-trace-sdk#readme> homepage:       https://www.instana.com/@@ -72,6 +72,7 @@       Instana.SDK.Span.NonRootEntry       Instana.SDK.Span.RootEntry       Instana.SDK.Span.Span+      Instana.SDK.Span.SpanType       Instana.SDK.TracingHeaders       Instana.Wai.Middleware.Entry   other-modules:@@ -87,7 +88,7 @@       Instana.SDK.Internal.Command       Instana.SDK.Internal.Config       Instana.SDK.Internal.Context-      Instana.SDK.Internal.FullSpan+      Instana.SDK.Internal.WireSpan       Instana.SDK.Internal.Id       Instana.SDK.Internal.Logging       Instana.SDK.Internal.Metrics.Collector@@ -206,6 +207,7 @@     , time     , transformers     , unix+    , unordered-containers     , wai     , warp   other-modules:@@ -264,6 +266,7 @@     , Instana.SDK.IntegrationTest.LowLevelApi     , Instana.SDK.IntegrationTest.Metrics     , Instana.SDK.IntegrationTest.Runner+    , Instana.SDK.IntegrationTest.ServiceName     , Instana.SDK.IntegrationTest.Suite     , Instana.SDK.IntegrationTest.TestHelper     , Instana.SDK.IntegrationTest.TestSuites
src/Instana/SDK/Config.hs view
@@ -13,6 +13,7 @@   , forceTransmissionAfter   , forceTransmissionStartingAt   , maxBufferedSpans+  , serviceName   ) where  @@ -33,6 +34,8 @@     -- changed the Server name on the agent side, you can use parameter to     -- provide the name to match that header against.   , agentName                   :: Maybe String+    -- | Overrides the default service name that is used in Instana.+  , serviceName                 :: Maybe String     -- | Spans are usually buffered before being transmitted to the agent. This     -- setting forces the transmission of all buffered spans after the given     -- amount of milliseconds. Default: 1000.@@ -58,6 +61,7 @@     { agentHost = Nothing     , agentPort = Nothing     , agentName = Nothing+    , serviceName = Nothing     , forceTransmissionAfter = Nothing     , forceTransmissionStartingAt = Nothing     , maxBufferedSpans = Nothing
src/Instana/SDK/Internal/Config.hs view
@@ -41,6 +41,11 @@ agentNameKey = "INSTANA_AGENT_NAME"  +-- |Environment variable for the service name override.+serviceNameKey :: String+serviceNameKey = "INSTANA_SERVICE_NAME"++ -- |Environment variable for the force-transmision-afeter setting forceTransmissionAfterKey :: String forceTransmissionAfterKey = "INSTANA_FORCE_TRANSMISSION_STARTING_AFTER"@@ -92,6 +97,7 @@   { agentHost                   :: String   , agentPort                   :: Int   , agentName                   :: String+  , serviceName                 :: Maybe String   , forceTransmissionAfter      :: Int   , forceTransmissionStartingAt :: Int   , maxBufferedSpans            :: Int@@ -103,6 +109,7 @@   String   -> Int   -> String+  -> Maybe String   -> Int   -> Int   -> Int@@ -111,6 +118,7 @@   agentHost_   agentPort_   agentName_+  serviceName_   forceTransmissionAfter_   forceTransmissionStartingAt_   maxBufferedSpans_ =@@ -118,6 +126,7 @@     { agentHost = agentHost_     , agentPort = agentPort_     , agentName = agentName_+    , serviceName = serviceName_     , forceTransmissionAfter = forceTransmissionAfter_     , forceTransmissionStartingAt = forceTransmissionStartingAt_     , maxBufferedSpans = maxBufferedSpans_@@ -130,6 +139,7 @@   agentHostEnv <- lookupEnv agentHostKey   agentPortEnv <- lookupEnv agentPortKey   agentNameEnv <- lookupEnv agentNameKey+  serviceNameEnv <- lookupEnv serviceNameKey   forceTransmissionAfterEnv <- lookupEnv forceTransmissionAfterKey   forceTransmissionStartingAtEnv <- lookupEnv forceTransmissionStartingAtKey   maxBufferedSpansEnv <- lookupEnv maxBufferedSpansKey@@ -147,6 +157,7 @@       { Config.agentHost = agentHostEnv       , Config.agentPort = agentPortParsed       , Config.agentName = agentNameEnv+      , Config.serviceName = serviceNameEnv       , Config.forceTransmissionAfter = forceTransmissionAfterParsed       , Config.forceTransmissionStartingAt = forceTransmissionStartingAtParsed       , Config.maxBufferedSpans = maxBufferedSpansParsed@@ -171,6 +182,7 @@        fromMaybe defaultAgentPort (Config.agentPort config)    , agentName =        fromMaybe defaultAgentName (Config.agentName config)+   , serviceName = Config.serviceName config    , forceTransmissionAfter =        fromMaybe          defaultForceTransmissionAfter@@ -200,6 +212,9 @@       , Config.agentName =           (Config.agentName userConfig) <|>           (Config.agentName configFromEnv)+      , Config.serviceName =+          (Config.serviceName userConfig) <|>+          (Config.serviceName configFromEnv)       , Config.forceTransmissionAfter =           (Config.forceTransmissionAfter userConfig) <|>           (Config.forceTransmissionAfter configFromEnv)
src/Instana/SDK/Internal/Context.hs view
@@ -30,9 +30,9 @@ import qualified Instana.SDK.Internal.AgentConnection.Json.AnnounceResponse as AnnounceResponse import           Instana.SDK.Internal.Command                               (Command) import           Instana.SDK.Internal.Config                                (FinalConfig)-import           Instana.SDK.Internal.FullSpan                              (FullSpan) import           Instana.SDK.Internal.Metrics.Sample                        (TimedSample) import           Instana.SDK.Internal.SpanStack                             (SpanStack)+import           Instana.SDK.Internal.WireSpan                              (QueuedSpan)   -- |The current state of the connection to the agent.@@ -113,7 +113,7 @@   , sdkStartTime          :: Int   , httpManager           :: HttpClient.Manager   , commandQueue          :: STM.TQueue Command-  , spanQueue             :: STM.TVar (Seq FullSpan)+  , spanQueue             :: STM.TVar (Seq QueuedSpan)   , connectionState       :: STM.TVar ConnectionState   , fileDescriptor        :: STM.TVar (Maybe CTypes.CInt)   , currentSpans          :: STM.TVar (Map ThreadId SpanStack)
− src/Instana/SDK/Internal/FullSpan.hs
@@ -1,145 +0,0 @@-{-# LANGUAGE DeriveGeneric     #-}-{-# LANGUAGE InstanceSigs      #-}-{-# LANGUAGE OverloadedStrings #-}-{-|-Module      : Instana.SDK.Internal.Context-Description : An internal representation of a span with all values set--}-module Instana.SDK.Internal.FullSpan-  ( FullSpan(..)-  , FullSpanWithPid(..)-  , SpanKind(..)-  ) where---import           Data.Aeson              (FromJSON, ToJSON, Value, (.:), (.=))-import qualified Data.Aeson              as Aeson-import           Data.Aeson.Types        (Parser)-import           Data.Text               (Text)-import           GHC.Generics--import           Instana.SDK.Internal.Id (Id)----- |Direction of the call.-data SpanKind =-    -- |The monitored componenent receives a call.-    Entry-    -- |The monitored componenent calls something else.-  | Exit-    -- |An additional annotation that is added to the trace while a traced call-    -- is being processed.-  | Intermediate-  deriving (Eq, Generic, Show)---instance FromJSON SpanKind where-  parseJSON :: Value -> Parser SpanKind-  parseJSON = Aeson.withScientific "span kind string" $-    \k ->-      case k of-        -- (1=entry, 2=exit, 3=local/intermediate)-        1 -> return Entry-        2 -> return Exit-        3 -> return Intermediate-        _              ->-          fail "expected numeric span kind (1, 2, or 3)."---instance ToJSON SpanKind where-  toJSON :: SpanKind -> Value-  toJSON k =-    case k of-      Entry        -> Aeson.Number 1-      Exit         -> Aeson.Number 2-      Intermediate -> Aeson.Number 3----- |The `from` part of the span.-data From = From-  { entityId :: String-  } deriving (Eq, Generic, Show)---instance FromJSON From where-  parseJSON = Aeson.withObject "from" $-    \f ->-      From-        <$> f .: "e" -- entityId---instance ToJSON From where-  toJSON :: From -> Value-  toJSON f = Aeson.object-    [ "e" .= entityId f ]----- |A representation of the span with all its data. This will be send to the--- agent later.-data FullSpan = FullSpan-  { traceId    :: Id-  , spanId     :: Id-  , parentId   :: Maybe Id-  , spanName   :: Text-  , timestamp  :: Int-  , duration   :: Int-  , kind       :: SpanKind-  , errorCount :: Int-  , spanData   :: Value-  } deriving (Eq, Generic, Show)----- |Combines the actual span data with static per-process data (PID).-data FullSpanWithPid = FullSpanWithPid-  { fullSpan :: FullSpan-  , pid      :: String-  } deriving (Eq, Generic, Show)----- Compare--- https://github.com/instana/technical-documentation/blob/master/tracing/format.md--- Registered and SDK spans use the same format, exact same attributes. The only--- difference is the data, SDK spans should provide data.sdk (see below).-instance ToJSON FullSpanWithPid where-  toJSON :: FullSpanWithPid -> Value-  toJSON fullSpanWithPid =-    let-      s = fullSpan fullSpanWithPid-      p = pid fullSpanWithPid-    in-    Aeson.object-      [ "t"     .= traceId s-      , "s"     .= spanId s-      , "p"     .= parentId s-      , "n"     .= spanName s-      , "ts"    .= timestamp s-      , "ta"    .= ("haskell" :: String)-      , "d"     .= duration s-      , "k"     .= kind s-      , "ec"    .= errorCount s-      , "data"  .= spanData s-      , "f"     .= From p-      -- TODO - missing attributes:-      -- - data.service - should have dedicated functionality to be set (for SDK-      --   spans)-      -- - For SDK spans: Everything should be in data.sdk, structure is described in-      --   https://github.com/instana/technical-documentation/blob/master/tracing/format.md#json-format-for-sdk-spans-      -- - e: [{ # events that happened during this span, seems to be used mostly by EUM??-      --     t: <long> # timestamp of this event relative to start (0 .. d)-      --     v: <String> # type of this annotation (ttfb, dom-ready, etc)-      --   }],-      -- - f.h: AgentId/HostID (optional), specified in the language announce response body.-      --   },-      -- - b: { # batching data-      --     s: <long> # size; amount of batched spans-      --     d: <long> # duration in ms; more realistic time of time consumed by individual batched spans. (optional, regular duration taken if absent)-      --   },-      -- - stack: [{ # stack trace-      --     c: <String> # Class name-      --     m: <String> # Method name-      --     n: <String> # Line number-      --     f: <String> # File name (optional in Java)-      --   }],-      -- - deferred: <boolean> # whether the span is deferred (optional), ??-      ]-
+ src/Instana/SDK/Internal/WireSpan.hs view
@@ -0,0 +1,144 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE InstanceSigs      #-}+{-# LANGUAGE OverloadedStrings #-}+{-|+Module      : Instana.SDK.Internal.Context+Description : Internal representations of a span with all values set, ready to+              be sent to the agent (over the wire, hence the name).+-}+module Instana.SDK.Internal.WireSpan+  ( QueuedSpan(..)+  , WireSpan(..)+  , SpanKind(..)+  ) where+++import           Control.Applicative     ((<|>))+import           Data.Aeson              (FromJSON, ToJSON, Value, (.:), (.=))+import qualified Data.Aeson              as Aeson+import qualified Data.Aeson.Extra.Merge  as AesonExtra+import           Data.Aeson.Types        (Parser)+import           Data.Text               (Text)+import           GHC.Generics++import           Instana.SDK.Internal.Id (Id)+++-- |Direction of the call.+data SpanKind =+    -- |The monitored componenent receives a call.+    Entry+    -- |The monitored componenent calls something else.+  | Exit+    -- |An additional annotation that is added to the trace while a traced call+    -- is being processed.+  | Intermediate+  deriving (Eq, Generic, Show)+++instance FromJSON SpanKind where+  parseJSON :: Value -> Parser SpanKind+  parseJSON = Aeson.withScientific "span kind string" $+    \k ->+      case k of+        -- (1=entry, 2=exit, 3=local/intermediate)+        1 -> return Entry+        2 -> return Exit+        3 -> return Intermediate+        _              ->+          fail "expected numeric span kind (1, 2, or 3)."+++instance ToJSON SpanKind where+  toJSON :: SpanKind -> Value+  toJSON k =+    case k of+      Entry        -> Aeson.Number 1+      Exit         -> Aeson.Number 2+      Intermediate -> Aeson.Number 3+++-- |The `from` part of the span.+data From = From+  { entityId :: String+  , hostId   :: Text+  } deriving (Eq, Generic, Show)+++instance FromJSON From where+  parseJSON = Aeson.withObject "from" $+    \f ->+      From+        <$> f .: "e" -- entityId+        <*> f .: "h" -- host ID/agent UUID+++instance ToJSON From where+  toJSON :: From -> Value+  toJSON f = Aeson.object+    [ "e" .= entityId f+    , "h" .= hostId f+    ]+++-- |A representation of the span with all its data, except for attributes that+-- are constant per Haskell process (pid/entityId and agent UUID/host ID). This+-- is a preliminary representation of what will be send to the agent later+-- (after adding the aforementioned per-process/static attributes). Values of+-- this type are stored in the spanQueue in Instana.SDK.Internal.Context after+-- they have been completed.+data QueuedSpan = QueuedSpan+  { traceId     :: Id+  , spanId      :: Id+  , parentId    :: Maybe Id+  , spanName    :: Text+  , timestamp   :: Int+  , duration    :: Int+  , kind        :: SpanKind+  , errorCount  :: Int+  , serviceName :: Maybe Text+  , spanData    :: Value+  } deriving (Eq, Generic, Show)+++-- |Combines the actual span data with static per-process data (PID,+-- agent UUID). This is the final value that will be sent to the agent.+data WireSpan = WireSpan+  { queuedSpan        :: QueuedSpan+  , pid               :: String+  , agentUuid         :: Text+  , serviceNameConfig :: Maybe Text+  } deriving (Eq, Generic, Show)+++instance ToJSON WireSpan where+  toJSON :: WireSpan -> Value+  toJSON wireSpan =+    let+      span_ = queuedSpan wireSpan+      pid_ = pid wireSpan+      agentUuid_ = agentUuid wireSpan+      serviceNameConfig_ = serviceNameConfig wireSpan+      spanData_ =+        case (serviceName span_ <|> serviceNameConfig_) of+          Just service ->+            AesonExtra.lodashMerge+              (spanData span_)+              (Aeson.object [ "service" .= service ])+          _ ->+            spanData span_+    in+    Aeson.object+      [ "t"     .= traceId span_+      , "s"     .= spanId span_+      , "p"     .= parentId span_+      , "n"     .= spanName span_+      , "ts"    .= timestamp span_+      , "ta"    .= ("haskell" :: String)+      , "d"     .= duration span_+      , "k"     .= kind span_+      , "ec"    .= errorCount span_+      , "data"  .= spanData_+      , "f"     .= From pid_ agentUuid_+      ]+
src/Instana/SDK/Internal/Worker.hs view
@@ -22,6 +22,7 @@ import           Data.List                                        (map) import           Data.Sequence                                    ((|>)) import qualified Data.Sequence                                    as Seq+import qualified Data.Text                                        as T import           Data.Time.Clock.POSIX                            (getPOSIXTime) import qualified Network.HTTP.Client                              as HTTP import qualified Network.HTTP.Types.Status                        as HttpTypes@@ -38,16 +39,16 @@                                                                    ConnectionState (..),                                                                    InternalContext) import qualified Instana.SDK.Internal.Context                     as InternalContext-import           Instana.SDK.Internal.FullSpan                    (FullSpan (FullSpan),-                                                                   FullSpanWithPid (FullSpanWithPid),-                                                                   SpanKind (Entry, Exit))-import qualified Instana.SDK.Internal.FullSpan                    as FullSpan import           Instana.SDK.Internal.Logging                     (instanaLogger) import qualified Instana.SDK.Internal.Metrics.Collector           as MetricsCollector import qualified Instana.SDK.Internal.Metrics.Compression         as MetricsCompression import qualified Instana.SDK.Internal.Metrics.Deltas              as Deltas import qualified Instana.SDK.Internal.Metrics.Sample              as Sample import qualified Instana.SDK.Internal.URL                         as URL+import           Instana.SDK.Internal.WireSpan                    (QueuedSpan (QueuedSpan),+                                                                   SpanKind (Entry, Exit),+                                                                   WireSpan (WireSpan))+import qualified Instana.SDK.Internal.WireSpan                    as WireSpan import           Instana.SDK.Span.EntrySpan                       (EntrySpan (..)) import qualified Instana.SDK.Span.EntrySpan                       as EntrySpan import           Instana.SDK.Span.ExitSpan                        (ExitSpan (..))@@ -127,16 +128,17 @@     timestamp = EntrySpan.timestamp entrySpan   queueSpan     context-    FullSpan-      { FullSpan.traceId    = EntrySpan.traceId entrySpan-      , FullSpan.spanId     = EntrySpan.spanId entrySpan-      , FullSpan.parentId   = EntrySpan.parentId entrySpan-      , FullSpan.spanName   = EntrySpan.spanName entrySpan-      , FullSpan.timestamp  = timestamp-      , FullSpan.duration   = now - timestamp-      , FullSpan.kind       = Entry-      , FullSpan.errorCount = EntrySpan.errorCount entrySpan-      , FullSpan.spanData   = EntrySpan.spanData entrySpan+    QueuedSpan+      { WireSpan.traceId     = EntrySpan.traceId entrySpan+      , WireSpan.spanId      = EntrySpan.spanId entrySpan+      , WireSpan.parentId    = EntrySpan.parentId entrySpan+      , WireSpan.spanName    = EntrySpan.spanName entrySpan+      , WireSpan.timestamp   = timestamp+      , WireSpan.duration    = now - timestamp+      , WireSpan.kind        = Entry+      , WireSpan.errorCount  = EntrySpan.errorCount entrySpan+      , WireSpan.serviceName = EntrySpan.serviceName entrySpan+      , WireSpan.spanData    = EntrySpan.spanData entrySpan       }  @@ -147,20 +149,21 @@   now <- round . (* 1000) <$> getPOSIXTime   queueSpan     context-    FullSpan-      { FullSpan.traceId    = EntrySpan.traceId parentSpan-      , FullSpan.spanId     = ExitSpan.spanId exitSpan-      , FullSpan.parentId   = Just $ EntrySpan.spanId parentSpan-      , FullSpan.spanName   = ExitSpan.spanName exitSpan-      , FullSpan.timestamp  = ExitSpan.timestamp exitSpan-      , FullSpan.duration   = now - ExitSpan.timestamp exitSpan-      , FullSpan.kind       = Exit-      , FullSpan.errorCount = ExitSpan.errorCount exitSpan-      , FullSpan.spanData   = ExitSpan.spanData exitSpan+    QueuedSpan+      { WireSpan.traceId     = EntrySpan.traceId parentSpan+      , WireSpan.spanId      = ExitSpan.spanId exitSpan+      , WireSpan.parentId    = Just $ EntrySpan.spanId parentSpan+      , WireSpan.spanName    = ExitSpan.spanName exitSpan+      , WireSpan.timestamp   = ExitSpan.timestamp exitSpan+      , WireSpan.duration    = now - ExitSpan.timestamp exitSpan+      , WireSpan.kind        = Exit+      , WireSpan.errorCount  = ExitSpan.errorCount exitSpan+      , WireSpan.serviceName = ExitSpan.serviceName exitSpan+      , WireSpan.spanData    = ExitSpan.spanData exitSpan       }  -queueSpan :: InternalContext -> FullSpan -> IO ()+queueSpan :: InternalContext -> QueuedSpan -> IO () queueSpan context span_ = do   currentSpanQueue <-     STM.atomically $@@ -220,7 +223,7 @@   spansSeq <- STM.atomically $     STM.swapTVar (InternalContext.spanQueue context) Seq.empty   let-    spans :: [FullSpan]+    spans :: [QueuedSpan]     spans = toList spansSeq   when (not $ null spans) $ do     InternalContext.whenConnected context $@@ -229,7 +232,7 @@  sendSpansToAgent ::   InternalContext-  -> [FullSpan]+  -> [QueuedSpan]   -> AgentConnection   -> Metrics.Store   -> IO ()@@ -238,16 +241,21 @@     agentHost = InternalContext.agentHost agentConnection     agentPort = InternalContext.agentPort agentConnection     translatedPidStr = InternalContext.pid agentConnection+    agentUuid = InternalContext.agentUuid agentConnection+    serviceNameConfig =+      T.pack <$> (InternalConfig.serviceName . InternalContext.config $ context)     traceEndpointUrl =       (show $         URL.mkHttp agentHost agentPort haskellTracePluginPath       ) ++ "." ++ translatedPidStr-    -- combine actual span data with static per-process data (e.g. PID)-    spansWithPid = map-      (\fullSpan ->-        FullSpanWithPid {-          FullSpan.fullSpan = fullSpan-        , FullSpan.pid      = translatedPidStr+    -- combine actual span data with static per-process data+    wireSpans = map+      (\queuedSpan ->+        WireSpan {+          WireSpan.queuedSpan        = queuedSpan+        , WireSpan.pid               = translatedPidStr+        , WireSpan.agentUuid         = agentUuid+        , WireSpan.serviceNameConfig = serviceNameConfig         }       ) spans   defaultRequestSettings <- HTTP.parseUrlThrow traceEndpointUrl@@ -255,7 +263,7 @@     request =       defaultRequestSettings         { HTTP.method = "POST"-        , HTTP.requestBody = HTTP.RequestBodyLBS $ Aeson.encode spansWithPid+        , HTTP.requestBody = HTTP.RequestBodyLBS $ Aeson.encode wireSpans         , HTTP.requestHeaders =           [ ("Accept", "application/json")           , ("Content-Type", "application/json; charset=UTF-8'")
src/Instana/SDK/SDK.hs view
@@ -11,9 +11,11 @@ module Instana.SDK.SDK     ( Config     , InstanaContext-    , addData-    , addDataAt+    , addRegisteredData+    , addRegisteredDataAt     , addHttpTracingHeaders+    , addTag+    , addTagAt     , addToErrorCount     , agentHost     , agentName@@ -28,6 +30,8 @@     , initInstana     , maxBufferedSpans     , readHttpTracingHeaders+    , serviceName+    , setServiceName     , startEntry     , startExit     , startHttpEntry@@ -91,6 +95,8 @@ import qualified Instana.SDK.Span.RootEntry          as RootEntry import           Instana.SDK.Span.Span               (Span (..), SpanKind (..)) import qualified Instana.SDK.Span.Span               as Span+import           Instana.SDK.Span.SpanType           (SpanType (RegisteredSpan))+import qualified Instana.SDK.Span.SpanType           as SpanType import           Instana.SDK.TracingHeaders          (TracingHeaders (TracingHeaders)) import qualified Instana.SDK.TracingHeaders          as TracingHeaders @@ -213,11 +219,11 @@ withRootEntry ::   MonadIO m =>   InstanaContext-  -> Text+  -> SpanType   -> m a   -> m a-withRootEntry context spanName io = do-  startRootEntry context spanName+withRootEntry context spanType io = do+  startRootEntry context spanType   result <- io   completeEntry context   return result@@ -229,11 +235,11 @@   InstanaContext   -> String   -> String-  -> Text+  -> SpanType   -> m a   -> m a-withEntry context traceId parentId spanName io = do-  startEntry context traceId parentId spanName+withEntry context traceId parentId spanType io = do+  startEntry context traceId parentId spanType   result <- io   completeEntry context   return result@@ -251,7 +257,7 @@   -> m a withHttpEntry context request io = do   let-    spanName = "haskell.wai.server"+    spanType = (RegisteredSpan SpanType.HaskellWaiServer)     tracingHeaders = readHttpTracingHeaders request     traceId = TracingHeaders.traceId tracingHeaders     spanId = TracingHeaders.spanId tracingHeaders@@ -262,9 +268,9 @@         io' = addDataFromRequest context request io       case (traceId, spanId) of         (Just t, Just s) ->-          withEntry context t s spanName io'+          withEntry context t s spanType io'         _                ->-          withRootEntry context spanName io'+          withRootEntry context spanType io'     TracingHeaders.Suppress -> do       liftIO $ pushSpan         context@@ -301,7 +307,7 @@       Wai.requestHeaderHost request       |> fmap BSC8.unpack       |> Maybe.fromMaybe ""-  addData+  addRegisteredData     context     (Aeson.object [ "http" .=       Aeson.object@@ -318,11 +324,11 @@ withExit ::   MonadIO m =>   InstanaContext-  -> Text+  -> SpanType   -> m a   -> m a-withExit context spanName io = do-  startExit context spanName+withExit context spanType io = do+  startExit context spanType   result <- io   completeExit context   return result@@ -349,9 +355,9 @@ startRootEntry ::   MonadIO m =>   InstanaContext-  -> Text+  -> SpanType   -> m ()-startRootEntry context spanName = do+startRootEntry context spanType = do   liftIO $ do     timestamp <- round . (* 1000) <$> getPOSIXTime     traceId <- Id.generate@@ -360,10 +366,11 @@         RootEntrySpan $           RootEntry             { RootEntry.spanAndTraceId = traceId-            , RootEntry.spanName       = spanName+            , RootEntry.spanName       = SpanType.spanName spanType             , RootEntry.timestamp      = timestamp             , RootEntry.errorCount     = 0-            , RootEntry.spanData       = emptyValue+            , RootEntry.serviceName    = Nothing+            , RootEntry.spanData       = SpanType.initialData EntryKind spanType             }     pushSpan       context@@ -385,9 +392,9 @@   InstanaContext   -> String   -> String-  -> Text+  -> SpanType   -> m ()-startEntry context traceId parentId spanName = do+startEntry context traceId parentId spanType = do   liftIO $ do     timestamp <- round . (* 1000) <$> getPOSIXTime     spanId <- Id.generate@@ -395,13 +402,14 @@       newSpan =         NonRootEntrySpan $           NonRootEntry-            { NonRootEntry.traceId    = Id.fromString traceId-            , NonRootEntry.spanId     = spanId-            , NonRootEntry.parentId   = Id.fromString parentId-            , NonRootEntry.spanName   = spanName-            , NonRootEntry.timestamp  = timestamp-            , NonRootEntry.errorCount = 0-            , NonRootEntry.spanData   = emptyValue+            { NonRootEntry.traceId     = Id.fromString traceId+            , NonRootEntry.spanId      = spanId+            , NonRootEntry.parentId    = Id.fromString parentId+            , NonRootEntry.spanName    = SpanType.spanName spanType+            , NonRootEntry.timestamp   = timestamp+            , NonRootEntry.errorCount  = 0+            , NonRootEntry.serviceName = Nothing+            , NonRootEntry.spanData    = SpanType.initialData EntryKind spanType             }     pushSpan       context@@ -428,7 +436,7 @@   -> m () startHttpEntry context request = do   let-    spanName = "haskell.wai.server"+    spanType = (RegisteredSpan SpanType.HaskellWaiServer)     tracingHeaders = readHttpTracingHeaders request     traceId = TracingHeaders.traceId tracingHeaders     spanId = TracingHeaders.spanId tracingHeaders@@ -437,10 +445,10 @@     TracingHeaders.Trace ->       case (traceId, spanId) of         (Just t, Just s) -> do-          startEntry context t s spanName+          startEntry context t s spanType           addHttpData context request         _                -> do-          startRootEntry context spanName+          startRootEntry context spanType           addHttpData context request     TracingHeaders.Suppress -> do       liftIO $ pushSpan@@ -460,9 +468,9 @@ startExit ::   MonadIO m =>   InstanaContext-  -> Text+  -> SpanType   -> m ()-startExit context spanName = do+startExit context spanType = do   liftIO $ do     suppressed <- isSuppressed context     if suppressed then@@ -478,10 +486,11 @@               ExitSpan                 { ExitSpan.parentSpan  = parent                 , ExitSpan.spanId      = spanId-                , ExitSpan.spanName    = spanName+                , ExitSpan.spanName    = SpanType.spanName spanType                 , ExitSpan.timestamp   = timestamp                 , ExitSpan.errorCount  = 0-                , ExitSpan.spanData    = emptyValue+                , ExitSpan.serviceName = Nothing+                , ExitSpan.spanData    = SpanType.initialData ExitKind spanType                 }           pushSpan             context@@ -498,12 +507,12 @@             )         Just (Exit ex) -> do           warningM instanaLogger $-            "Cannot start exit span \"" ++ show spanName +++            "Cannot start exit span \"" ++ show spanType ++             "\" since there is already an active exit span " ++             "in progress: " ++ show ex         Nothing -> do           warningM instanaLogger $-            "Cannot start exit span \"" ++ show spanName +++            "Cannot start exit span \"" ++ show spanType ++             "\" since there is no active entry span " ++             "(actually, there is no active span at all)."           return ()@@ -531,7 +540,7 @@                 res                   |> HTTP.responseStatus                   |> HTTPTypes.statusCode-            addData context+            addRegisteredData context               (Aeson.object [ "http" .=                 Aeson.object                   [ "status" .= status@@ -547,8 +556,8 @@     path = BSC8.unpack $ HTTP.path request     url = protocol ++ host ++ port ++ path -  startExit context "haskell.http.client"-  addData+  startExit context (RegisteredSpan SpanType.HaskellHttpClient)+  addRegisteredData     context     (Aeson.object [ "http" .=       Aeson.object@@ -657,32 +666,88 @@     (\span_ -> Span.addToErrorCount increment span_)  --- |Adds additional custom data to the currently active span. Call this+-- |Override the name of the service for the associated call in Instana.+setServiceName :: MonadIO m => InstanaContext -> Text -> m ()+setServiceName context serviceName_ =+  liftIO $ modifyCurrentSpan context+    (\span_ -> Span.setServiceName serviceName_ span_)+++-- |Adds additional custom tags to the currently active span. Call this -- between startEntry/startRootEntry/startExit and completeEntry/completeExit or -- inside the IO action given to with withEntry/withExit/withRootEntry. -- The given path can be a nested path, with path fragments separated by dots, -- like "http.url". This will result in -- "data": { --   ...+--   "sdk": {+--     "custom": {+--       "tags": {+--         "http": {+--           "url": "..."+--         },+--       },+--     },+--   },+--   ...+-- }+--+-- This should be used for SDK spans instead of addRegisteredDataAt.+addTagAt :: (MonadIO m, Aeson.ToJSON a) => InstanaContext -> Text -> a -> m ()+addTagAt context path value =+  liftIO $ modifyCurrentSpan context+    (\span_ -> Span.addTagAt path value span_)+++-- |Adds additional custom tags to the currently active span. Call this+-- between startEntry/startRootEntry/startExit and completeEntry/completeExit or+-- inside the IO action given to with withEntry/withExit/withRootEntry. Can be+-- called multiple times, data from multiple calls will be merged.+--+-- This should be used for SDK spans instead of addRegisteredData.+addTag :: MonadIO m => InstanaContext -> Value -> m ()+addTag context value =+  liftIO $ modifyCurrentSpan context+    (\span_ -> Span.addTag value span_)+++-- |Adds additional meta data to the currently active registered span. Call this+-- between startEntry/startRootEntry/startExit and completeEntry/completeExit or+-- inside the IO action given to with withEntry/withExit/withRootEntry.+-- The given path can be a nested path, with path fragments separated by dots,+-- like "http.url". This will result in+-- "data": {+--   ... --   "http": { --     "url": "..." --   }, --   ... -- }-addDataAt :: (MonadIO m, Aeson.ToJSON a) => InstanaContext -> Text -> a -> m ()-addDataAt context path value =+--+-- Note that this should only be used for registered spans, not for SDK spans.+-- Use addTagAt for SDK spans instead.+addRegisteredDataAt ::+  (MonadIO m, Aeson.ToJSON a) =>+  InstanaContext+  -> Text+  -> a+  -> m ()+addRegisteredDataAt context path value =   liftIO $ modifyCurrentSpan context-    (\span_ -> Span.addDataAt path value span_)+    (\span_ -> Span.addRegisteredDataAt path value span_)  --- |Adds additional custom data to the currently active span. Call this+-- |Adds additional data to the currently active registered span. Call this -- between startEntry/startRootEntry/startExit and completeEntry/completeExit or -- inside the IO action given to with withEntry/withExit/withRootEntry. Can be -- called multiple times, data from multiple calls will be merged.-addData :: MonadIO m => InstanaContext -> Value -> m ()-addData context value =+--+-- Note that this should only be used for registered spans, not for SDK spans.+-- Use addTag for SDK spans instead.+addRegisteredData :: MonadIO m => InstanaContext -> Value -> m ()+addRegisteredData context value =   liftIO $ modifyCurrentSpan context-    (\span_ -> Span.addData value span_)+    (\span_ -> Span.addRegisteredData value span_)   -- |Reads the Instana tracing headers@@ -882,9 +947,4 @@   Maybe.fromMaybe     SpanStack.empty     ((SpanStack.mapTop fn) <$> stack)----- |Provides an empty Aeson value.-emptyValue :: Value-emptyValue = Aeson.object [] 
src/Instana/SDK/Span/EntrySpan.hs view
@@ -11,6 +11,8 @@   , spanName   , timestamp   , errorCount+  , serviceName+  , setServiceName   , spanData   , addData   , addToErrorCount@@ -91,6 +93,24 @@       RootEntrySpan $ RootEntry.addToErrorCount increment entry     NonRootEntrySpan entry ->       NonRootEntrySpan $ NonRootEntry.addToErrorCount increment entry+++-- |An optional attribute for overriding the name of the service in Instana.+serviceName :: EntrySpan -> Maybe Text+serviceName entrySpan =+  case entrySpan of+    RootEntrySpan entry    -> RootEntry.serviceName entry+    NonRootEntrySpan entry -> NonRootEntry.serviceName entry+++-- |Override the name of the service for the associated call in Instana.+setServiceName :: Text -> EntrySpan -> EntrySpan+setServiceName serviceName_ entrySpan =+  case entrySpan of+    RootEntrySpan entry ->+      RootEntrySpan $ RootEntry.setServiceName serviceName_ entry+    NonRootEntrySpan entry ->+      NonRootEntrySpan $ NonRootEntry.setServiceName serviceName_ entry   -- |Optional additional span data.
src/Instana/SDK/Span/ExitSpan.hs view
@@ -9,6 +9,7 @@   , traceId   , addData   , addToErrorCount+  , setServiceName   ) where  @@ -27,19 +28,22 @@   ExitSpan     {       -- |The parent span-      parentSpan :: EntrySpan+      parentSpan  :: EntrySpan       -- |The span ID-    , spanId     :: Id-      -- |The span name/type, e.g. a short string like "yesod", "servant",-      -- "rpc-server", ...-    , spanName   :: Text+    , spanId      :: Id+      -- |The span name/type, e.g. a short string like "haskell.wai.server",+      -- "haskell.http.client". For SDK spans this is always "sdk", the actual+      -- name is then in span.data.sdk.name.+    , spanName    :: Text       -- |The time the span started-    , timestamp  :: Int+    , timestamp   :: Int+      -- |An attribute for overriding the name of the service in Instana+    , serviceName :: Maybe Text       -- |The number of errors that occured during processing-    , errorCount :: Int+    , errorCount  :: Int       -- |Additional data for the span. Must be provided as an       -- 'Data.Aeson.Value'.-    , spanData   :: Value+    , spanData    :: Value     } deriving (Eq, Generic, Show)  @@ -62,6 +66,12 @@     ec = errorCount exitSpan   in   exitSpan { errorCount = ec + increment }+++-- |Override the name of the service for the associated call in Instana.+setServiceName :: Text -> ExitSpan -> ExitSpan+setServiceName serviceName_ exitSpan =+  exitSpan { serviceName = Just serviceName_ }   -- |Add a value to the span's data section.
src/Instana/SDK/Span/NonRootEntry.hs view
@@ -7,6 +7,7 @@   ( NonRootEntry(..)   , addData   , addToErrorCount+  , setServiceName   ) where  @@ -23,21 +24,24 @@   NonRootEntry     {       -- |The trace ID-      traceId    :: Id+      traceId     :: Id       -- |The span ID-    , spanId     :: Id+    , spanId      :: Id       -- |The ID of the parent span-    , parentId   :: Id-      -- |The span name/type, e.g. a short string like "yesod", "servant",-      -- "rpc-server", ...-    , spanName   :: Text+    , parentId    :: Id+      -- |The span name/type, e.g. a short string like "haskell.wai.server",+      -- "haskell.http.client". For SDK spans this is always "sdk", the actual+      -- name is then in span.data.sdk.name.+    , spanName    :: Text       -- |The time the span started-    , timestamp  :: Int+    , timestamp   :: Int       -- |The number of errors that occured during processing-    , errorCount :: Int+    , errorCount  :: Int+      -- |An attribute for overriding the name of the service in Instana+    , serviceName :: Maybe Text       -- |Additional data for the span. Must be provided as an       -- 'Data.Aeson.Value'.-    , spanData   :: Value+    , spanData    :: Value     } deriving (Eq, Generic, Show)  @@ -48,6 +52,12 @@     ec = errorCount nonRootEntry   in   nonRootEntry { errorCount = ec + increment }+++-- |Override the name of the service for the associated call in Instana.+setServiceName :: Text -> NonRootEntry -> NonRootEntry+setServiceName serviceName_ nonRootEntry =+  nonRootEntry { serviceName = Just serviceName_ }   -- |Add a value to the span's data section.
src/Instana/SDK/Span/RootEntry.hs view
@@ -9,6 +9,7 @@   , traceId   , addData   , addToErrorCount+  , setServiceName   ) where  @@ -26,12 +27,16 @@     {       -- |The trace ID and span ID (those are identical for root spans)       spanAndTraceId :: Id-      -- |The span name/type, e.g. a short string like "yesod", "servant",+      -- |The span name/type, e.g. a short string like "haskell.wai.server",+      -- "haskell.http.client". For SDK spans this is always "sdk", the actual+      -- name is then in span.data.sdk.name.     , spanName       :: Text       -- |The time the span (and trace) started     , timestamp      :: Int       -- |The number of errors that occured during processing     , errorCount     :: Int+      -- |An attribute for overriding the name of the service in Instana+    , serviceName    :: Maybe Text       -- |Additional data for the span. Must be provided as an       -- 'Data.Aeson.Value'.     , spanData       :: Value@@ -55,6 +60,12 @@     ec = errorCount rootEntry   in   rootEntry { errorCount = ec + increment }+++-- |Override the name of the service for the associated call in Instana.+setServiceName :: Text -> RootEntry -> RootEntry+setServiceName serviceName_ rootEntry =+  rootEntry { serviceName = Just serviceName_ }   -- |Add a value to the span's data section.
src/Instana/SDK/Span/Span.hs view
@@ -2,11 +2,11 @@ {-# LANGUAGE OverloadedStrings #-} {-| Module      : Instana.SDK.Span.Span-Description : A type class for spans (entries and exits)+Description : A type for spans (entries, exits or intermediates). -} module Instana.SDK.Span.Span   ( Span (Entry, Exit)-  , SpanKind (EntryKind, ExitKind)+  , SpanKind (EntryKind, ExitKind, IntermediateKind)   , traceId   , spanId   , spanKind@@ -15,9 +15,13 @@   , timestamp   , errorCount   , addToErrorCount+  , serviceName+  , setServiceName   , spanData-  , addData-  , addDataAt+  , addRegisteredData+  , addRegisteredDataAt+  , addTag+  , addTagAt   ) where  @@ -102,7 +106,7 @@     Exit exit   -> ExitSpan.timestamp exit  --- |Start time.+-- |Error count. errorCount :: Span -> Int errorCount span_ =   case span_ of@@ -120,6 +124,24 @@       Exit $ ExitSpan.addToErrorCount increment exit  +-- |An optional attribute for overriding the name of the service in Instana.+serviceName :: Span -> Maybe Text+serviceName span_ =+  case span_ of+    Entry entry -> EntrySpan.serviceName entry+    Exit exit   -> ExitSpan.serviceName exit+++-- |Override the name of the service for the associated call in Instana.+setServiceName :: Text -> Span -> Span+setServiceName serviceName_ span_ =+  case span_ of+    Entry entry ->+      Entry $ EntrySpan.setServiceName serviceName_ entry+    Exit exit ->+      Exit $ ExitSpan.setServiceName serviceName_ exit++ -- |Optional additional span data. spanData :: Span -> Value spanData span_ =@@ -128,17 +150,35 @@     Exit exit   -> ExitSpan.spanData exit  --- |Add a value to the span's data section.-addData :: Value -> Span -> Span-addData value span_ =+-- |Add a value to the span's custom tags section. This should be used for SDK+-- spans instead of addRegisteredData.+addTag :: Value -> Span -> Span+addTag value span_ =+  addRegisteredDataAt "sdk.custom.tags" value span_+++-- |Add a value to the given path to the span's custom tags section. This should+-- be used for SDK spans instead of addRegisteredDataAt.+addTagAt :: Aeson.ToJSON a => Text -> a -> Span -> Span+addTagAt path value span_ =+  addRegisteredDataAt (T.concat ["sdk.custom.tags.", path]) value span_++++-- |Add a value to the span's data section. This should only be used for+-- registered spans, not for SDK spans. For SDK spans, you should use addTag+-- instead.+addRegisteredData :: Value -> Span -> Span+addRegisteredData value span_ =   case span_ of     Entry entry -> Entry $ EntrySpan.addData value entry     Exit exit   -> Exit $ ExitSpan.addData value exit  --- |Add a value at the given path to the span's data section.-addDataAt :: Aeson.ToJSON a => Text -> a -> Span -> Span-addDataAt path value span_ =+-- |Add a value at the given path to the span's data section. For SDK spans, you+-- should use addTagAt instead.+addRegisteredDataAt :: Aeson.ToJSON a => Text -> a -> Span -> Span+addRegisteredDataAt path value span_ =   let     pathSegments = T.splitOn "." path     newData = List.foldr@@ -150,5 +190,5 @@       (Aeson.toJSON value)       pathSegments   in-  addData newData span_+  addRegisteredData newData span_ 
+ src/Instana/SDK/Span/SpanType.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE InstanceSigs      #-}+{-# LANGUAGE OverloadedStrings #-}+{-|+Module      : Instana.SDK.Span.SpanType+Description : Describes the type of a span, either an SDK span or a+              registered span.+-}+module Instana.SDK.Span.SpanType+  ( Registered (..)+  , SpanType (SdkSpan, RegisteredSpan)+  , spanName+  , initialData+  ) where+++import           Data.Aeson            (Value, (.=))+import qualified Data.Aeson            as Aeson+import           Data.String           (IsString (fromString))+import           Data.Text             (Text)+import qualified Data.Text             as T+import           GHC.Generics++import           Instana.SDK.Span.Span (SpanKind (EntryKind, ExitKind, IntermediateKind))+++-- |Differentiates between SDK spans and registered spans (which receive+-- special treatment by Instana's processing pipeline.+data SpanType =+    SdkSpan Text+  | RegisteredSpan Registered+  deriving (Eq, Generic, Show)+++-- |All registered spans that the Haskell trace SDK will produce.+data Registered =+    HaskellWaiServer+  | HaskellHttpClient+  deriving (Eq, Generic, Show)+++-- |Returns the wire value of span.n for a SpanType value.+spanName    :: SpanType -> Text+spanName    (SdkSpan _)                 = "sdk"+spanName    (RegisteredSpan registered) = registeredSpanName registered+++-- |Returns the wire value of span.n for a registered span.+registeredSpanName :: Registered -> Text+registeredSpanName HaskellWaiServer  = "haskell.wai.server"+registeredSpanName HaskellHttpClient = "haskell.http.client"+++-- |Returns the initial data (span.data) for a SpanType value.+initialData :: SpanKind -> SpanType -> Value+initialData kind (SdkSpan s)     = initialSdkData kind s+initialData _ (RegisteredSpan _) = emptyValue+++-- |Enables passing any string as the span type argument to SDK.startEntrySpan+-- etc. - this will be automatically converted to an SDK span.+instance IsString SpanType where+  fromString :: String -> SpanType+  fromString s = SdkSpan $ T.pack s+++-- |Provides the initial data for an SDK span.+initialSdkData :: SpanKind -> Text -> Value+initialSdkData kind spanName_ =+  let+    sdkKind :: String+    sdkKind =+      case kind of+        EntryKind        -> "entry"+        ExitKind         -> "exit"+        IntermediateKind -> "intermediate"+  in+  (Aeson.object [ "sdk" .=+    Aeson.object+      [ "name" .= spanName_+      , "type" .= sdkKind+      ]+    ]+  )+++-- |Provides an empty Aeson value.+emptyValue :: Value+emptyValue = Aeson.object []+
test/apps/wai/Main.hs view
@@ -15,6 +15,7 @@ import qualified Data.Text                  as T import           Instana.SDK.SDK            (InstanaContext) import qualified Instana.SDK.SDK            as InstanaSDK+import qualified Instana.SDK.Span.SpanType  as SpanType import qualified Network.HTTP.Client        as HTTP import qualified Network.HTTP.Types         as HTTPTypes import qualified Network.Wai                as Wai@@ -52,14 +53,18 @@       bracketApiRootEntry instana respond     ("POST", ["bracket", "api", "non-root"]) ->       bracketApiNonRootEntry instana respond-    ("POST", ["bracket", "api", "with-data"]) ->-      bracketApiWithData instana respond+    ("POST", ["bracket", "api", "with-tags"]) ->+      bracketApiWithTags instana respond+    ("POST", ["bracket", "api", "with-service-name"]) ->+      bracketApiWithServiceName instana respond+    ("POST", ["bracket", "api", "with-service-name-exit-only"]) ->+      bracketApiWithServiceNameExitOnly instana respond     ("POST", ["low", "level", "api", "root"]) ->       lowLevelApiRootEntry instana respond     ("POST", ["low", "level", "api", "non-root"]) ->       lowLevelApiNonRootEntry instana respond-    ("POST", ["low", "level", "api", "with-data"]) ->-      lowLevelApiWithData instana respond+    ("POST", ["low", "level", "api", "with-tags"]) ->+      lowLevelApiWithTags instana respond     ("GET", ["http", "bracket", "api"]) ->       httpBracketApi instana httpManager request respond     ("GET", ["http", "low", "level", "api"]) ->@@ -126,45 +131,100 @@     simulateExitCall  -bracketApiWithData ::+bracketApiWithTags ::   InstanaContext   -> (Wai.Response -> IO Wai.ResponseReceived)   -> IO Wai.ResponseReceived-bracketApiWithData instana respond = do+bracketApiWithTags instana respond = do   entryCallResult <-     InstanaSDK.withRootEntry       instana       "haskell.dummy.root.entry"-      (withExitWithData instana)+      (withExitWithTags instana)   respondWithPlainText respond $ entryCallResult  -withExitWithData :: InstanaContext -> IO String-withExitWithData instana = do-  InstanaSDK.addData instana (someSpanData "entry")+withExitWithTags :: InstanaContext -> IO String+withExitWithTags instana = do+  InstanaSDK.addTag instana (someSpanData "entry")   exitCallResult <-     InstanaSDK.withExit       instana       "haskell.dummy.exit"-      (simulateExitCallWithData instana)+      (simulateExitCallWithTags instana)   InstanaSDK.incrementErrorCount instana-  InstanaSDK.addData instana (moreSpanData "entry")+  InstanaSDK.addTag instana (moreSpanData "entry")   return $ exitCallResult ++ "::entry done"  -simulateExitCallWithData :: InstanaContext -> IO String-simulateExitCallWithData instana = do-  InstanaSDK.addData instana (someSpanData "exit")+simulateExitCallWithTags :: InstanaContext -> IO String+simulateExitCallWithTags instana = do+  InstanaSDK.addTag instana (someSpanData "exit")   -- some time needs to pass, otherwise the exit span's duration will be 0   threadDelay $ 10 * 1000   InstanaSDK.addToErrorCount instana 2-  InstanaSDK.addData instana (moreSpanData "exit")-  InstanaSDK.addDataAt instana "nested.key1" ("nested.text.value1" :: String)-  InstanaSDK.addDataAt instana "nested.key2" ("nested.text.value2" :: String)-  InstanaSDK.addDataAt instana "nested.key3" (1604 :: Int)+  InstanaSDK.addTag instana (moreSpanData "exit")+  InstanaSDK.addTagAt instana "nested.key1" ("nested.text.value1" :: String)+  InstanaSDK.addTagAt instana "nested.key2" ("nested.text.value2" :: String)+  InstanaSDK.addTagAt instana "nested.key3" (1604 :: Int)   return "exit done"  +bracketApiWithServiceName ::+  InstanaContext+  -> (Wai.Response -> IO Wai.ResponseReceived)+  -> IO Wai.ResponseReceived+bracketApiWithServiceName instana respond = do+  entryCallResult <-+    InstanaSDK.withRootEntry+      instana+      "haskell.dummy.root.entry"+      (withExitWithServiceName instana)+  respondWithPlainText respond $ entryCallResult+++withExitWithServiceName :: InstanaContext -> IO String+withExitWithServiceName instana = do+  InstanaSDK.setServiceName instana "Service Entry"+  exitCallResult <-+    InstanaSDK.withExit+      instana+      "haskell.dummy.exit"+      (simulateExitCallWithServiceName instana)+  return $ exitCallResult ++ "::entry done"+++bracketApiWithServiceNameExitOnly ::+  InstanaContext+  -> (Wai.Response -> IO Wai.ResponseReceived)+  -> IO Wai.ResponseReceived+bracketApiWithServiceNameExitOnly instana respond = do+  entryCallResult <-+    InstanaSDK.withRootEntry+      instana+      "haskell.dummy.root.entry"+      (withExitWithServiceNameExitOnly instana)+  respondWithPlainText respond $ entryCallResult+++withExitWithServiceNameExitOnly :: InstanaContext -> IO String+withExitWithServiceNameExitOnly instana = do+  exitCallResult <-+    InstanaSDK.withExit+      instana+      "haskell.dummy.exit"+      (simulateExitCallWithServiceName instana)+  return $ exitCallResult ++ "::entry done"+++simulateExitCallWithServiceName :: InstanaContext -> IO String+simulateExitCallWithServiceName instana = do+  InstanaSDK.setServiceName instana "Service Exit"+  -- some time needs to pass, otherwise the exit span's duration will be 0+  threadDelay $ 10 * 1000+  return "exit done"++ lowLevelApiRootEntry ::   InstanaContext   -> (Wai.Response -> IO Wai.ResponseReceived)@@ -203,34 +263,34 @@   return result  -lowLevelApiWithData ::+lowLevelApiWithTags ::   InstanaContext   -> (Wai.Response -> IO Wai.ResponseReceived)   -> IO Wai.ResponseReceived-lowLevelApiWithData instana respond = do+lowLevelApiWithTags instana respond = do   InstanaSDK.startRootEntry     instana     "haskell.dummy.root.entry"-  InstanaSDK.addData instana (someSpanData "entry")-  result <- doExitCallWithData instana+  InstanaSDK.addTag instana (someSpanData "entry")+  result <- doExitCallWithTags instana   InstanaSDK.incrementErrorCount instana-  InstanaSDK.addData instana (moreSpanData "entry")-  InstanaSDK.addDataAt+  InstanaSDK.addTag instana (moreSpanData "entry")+  InstanaSDK.addTagAt     instana "nested.entry.key" ("nested.entry.value" :: String)   InstanaSDK.completeEntry instana   respondWithPlainText respond result  -doExitCallWithData :: InstanaContext -> IO String-doExitCallWithData instana = do+doExitCallWithTags :: InstanaContext -> IO String+doExitCallWithTags instana = do   InstanaSDK.startExit     instana     "haskell.dummy.exit"-  InstanaSDK.addData instana (someSpanData "exit")+  InstanaSDK.addTag instana (someSpanData "exit")   result <- simulateExitCall   InstanaSDK.incrementErrorCount instana-  InstanaSDK.addData instana (moreSpanData "exit")-  InstanaSDK.addDataAt instana "nested.exit.key" ("nested.exit.value" :: String)+  InstanaSDK.addTag instana (moreSpanData "exit")+  InstanaSDK.addTagAt instana "nested.exit.key" ("nested.exit.value" :: String)   InstanaSDK.completeExit instana   return result @@ -320,8 +380,8 @@     maybeMaybeSpanName = lookup ("spanName" :: ByteString) query     spanNameByteString =       Maybe.fromMaybe "haskell.test.span" $ join maybeMaybeSpanName-    spanName = T.pack $ BS.unpack spanNameByteString-  InstanaSDK.withRootEntry instana spanName $ do+    spanType = SpanType.SdkSpan $ T.pack $ BS.unpack spanNameByteString+  InstanaSDK.withRootEntry instana spanType $ do     threadDelay $ 1000 -- make sure there is a duration > 0   respond $     Wai.responseBuilder
test/integration/Instana/SDK/IntegrationTest/BracketApi.hs view
@@ -2,7 +2,8 @@ module Instana.SDK.IntegrationTest.BracketApi   ( shouldRecordSpans   , shouldRecordNonRootEntry-  , shouldMergeData+  , shouldMergeTags+  , shouldSetServiceName   ) where  @@ -25,7 +26,7 @@ shouldRecordSpans pid =   applyLabel "shouldRecordSpans" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (result, spansResults) <-       TestHelper.withSpanCreation         createRootEntry@@ -38,8 +39,8 @@       Right spans -> do         let           maybeRootEntrySpan =-            TestHelper.getSpanByName "haskell.dummy.root.entry" spans-          maybeExitSpan = TestHelper.getSpanByName "haskell.dummy.exit" spans+            TestHelper.getSpanBySdkName "haskell.dummy.root.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans         if isNothing maybeRootEntrySpan || isNothing maybeExitSpan           then             failIO "expected spans have not been recorded"@@ -83,7 +84,7 @@ shouldRecordNonRootEntry pid =   applyLabel "shouldRecordNonRootEntry" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (result, spansResults) <-       TestHelper.withSpanCreation         createNonRootEntry@@ -95,8 +96,8 @@         failIO $ "Could not load recorded spans from agent stub: " ++ failure       Right spans -> do         let-          maybeEntrySpan = TestHelper.getSpanByName "haskell.dummy.entry" spans-          maybeExitSpan = TestHelper.getSpanByName "haskell.dummy.exit" spans+          maybeEntrySpan = TestHelper.getSpanBySdkName "haskell.dummy.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans         if isNothing maybeEntrySpan || isNothing maybeExitSpan           then             failIO "expected spans have not been recorded"@@ -138,14 +139,14 @@   return $ LBSC8.unpack $ HTTP.responseBody response  -shouldMergeData :: String -> IO Test-shouldMergeData pid =-  applyLabel "shouldMergeData" $ do+shouldMergeTags :: String -> IO Test+shouldMergeTags pid =+  applyLabel "shouldMergeTags" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (result, spansResults) <-       TestHelper.withSpanCreation-        createSpansWithData+        createSpansWithTags         [ "haskell.dummy.root.entry"         , "haskell.dummy.exit"         ]@@ -155,8 +156,8 @@       Right spans -> do         let           maybeRootEntrySpan =-            TestHelper.getSpanByName "haskell.dummy.root.entry" spans-          maybeExitSpan = TestHelper.getSpanByName "haskell.dummy.exit" spans+            TestHelper.getSpanBySdkName "haskell.dummy.root.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans         if isNothing maybeRootEntrySpan || isNothing maybeExitSpan           then             failIO "expected spans have not been recorded"@@ -183,43 +184,136 @@               , assertEqual "entry error" 1 (TraceRequest.ec rootEntrySpan)               , assertEqual "entry from" from $ TraceRequest.f rootEntrySpan               , assertEqual "entry data"-                  ( Aeson.object-                    [ "data1"     .= ("value1" :: String)-                    , "data2"     .= (1302 :: Int)-                    , "startKind" .= ("entry" :: String)-                    , "data2"     .= (1302 :: Int)-                    , "data3"     .= ("value3" :: String)-                    , "endKind"   .= ("entry" :: String)-                    ]-                  )-                  (TraceRequest.spanData rootEntrySpan)+                ( Aeson.object+                  [ "sdk"    .= (Aeson.object+                    [ "name" .= ("haskell.dummy.root.entry" :: String)+                    , "type" .= ("entry" :: String)+                    , "custom" .= (Aeson.object+                      [ "tags" .= (Aeson.object+                        [ "data1"     .= ("value1" :: String)+                        , "data2"     .= (1302 :: Int)+                        , "startKind" .= ("entry" :: String)+                        , "data2"     .= (1302 :: Int)+                        , "data3"     .= ("value3" :: String)+                        , "endKind"   .= ("entry" :: String)+                        ])+                      ])+                    ])+                  ]+                )+                (TraceRequest.spanData rootEntrySpan)               , assertBool "exit timestamp" $ TraceRequest.ts exitSpan > 0               , assertBool "exit duration" $ TraceRequest.d exitSpan > 0               , assertEqual "exit kind" 2 (TraceRequest.k exitSpan)               , assertEqual "exit error" 2 (TraceRequest.ec exitSpan)               , assertEqual "exit from" from $ TraceRequest.f exitSpan               , assertEqual "exit data"-                  ( Aeson.object-                    [ "data1"     .= ("value1" :: String)-                    , "data2"     .= (1302 :: Int)-                    , "startKind" .= ("exit" :: String)-                    , "data2"     .= (1302 :: Int)-                    , "data3"     .= ("value3" :: String)-                    , "endKind"   .= ("exit" :: String)-                    , "nested"    .= (Aeson.object-                        [ "key1" .= ("nested.text.value1" :: String)-                        , "key2" .= ("nested.text.value2" :: String)-                        , "key3" .= (1604 :: Integer)-                        ]-                      )-                    ]-                  )-                  (TraceRequest.spanData exitSpan)+                ( Aeson.object+                  [ "sdk"    .= (Aeson.object+                    [ "name" .= ("haskell.dummy.exit" :: String)+                    , "type" .= ("exit" :: String)+                    , "custom" .= (Aeson.object+                      [ "tags" .= (Aeson.object+                        [ "data1"     .= ("value1" :: String)+                        , "data2"     .= (1302 :: Int)+                        , "startKind" .= ("exit" :: String)+                        , "data2"     .= (1302 :: Int)+                        , "data3"     .= ("value3" :: String)+                        , "endKind"   .= ("exit" :: String)+                        , "nested"    .= (Aeson.object+                            [ "key1" .= ("nested.text.value1" :: String)+                            , "key2" .= ("nested.text.value2" :: String)+                            , "key3" .= (1604 :: Integer)+                            ])+                        ])+                      ])+                    ])+                  ]+                )+                (TraceRequest.spanData exitSpan)               ]  -createSpansWithData :: IO String-createSpansWithData = do-  response <- HttpHelper.doAppRequest "bracket/api/with-data" "POST" []+createSpansWithTags :: IO String+createSpansWithTags = do+  response <- HttpHelper.doAppRequest "bracket/api/with-tags" "POST" []+  return $ LBSC8.unpack $ HTTP.responseBody response+++shouldSetServiceName :: String -> IO Test+shouldSetServiceName pid =+  applyLabel "shouldSetServiceName" $ do+    let+      from = Just $ From pid "agent-stub-id"+    (result, spansResults) <-+      TestHelper.withSpanCreation+        createSpansWithServiceName+        [ "haskell.dummy.root.entry"+        , "haskell.dummy.exit"+        ]+    case spansResults of+      Left failure ->+        failIO $ "Could not load recorded spans from agent stub: " ++ failure+      Right spans -> do+        let+          maybeRootEntrySpan =+            TestHelper.getSpanBySdkName "haskell.dummy.root.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans+        if isNothing maybeRootEntrySpan || isNothing maybeExitSpan+          then+            failIO "expected spans have not been recorded"+          else do+            let+              Just rootEntrySpan = maybeRootEntrySpan+              Just exitSpan = maybeExitSpan+            assertAllIO+              [ assertEqual "result" "exit done::entry done" result+              , assertEqual "trace ID is consistent"+                  (TraceRequest.t rootEntrySpan)+                  (TraceRequest.t exitSpan)+              , assertEqual "root.traceId == root.spanId"+                  (TraceRequest.s rootEntrySpan)+                  (TraceRequest.t rootEntrySpan)+              , assertBool "root has no parent" $+                  isNothing $ TraceRequest.p rootEntrySpan+              , assertEqual "exit parent"+                  (Just $ TraceRequest.s rootEntrySpan)+                  (TraceRequest.p exitSpan)+              , assertBool "entry timestamp" $ TraceRequest.ts rootEntrySpan > 0+              , assertBool "entry duration" $ TraceRequest.d rootEntrySpan > 0+              , assertEqual "entry kind" 1 (TraceRequest.k rootEntrySpan)+              , assertEqual "entry error" 0 (TraceRequest.ec rootEntrySpan)+              , assertEqual "entry from" from $ TraceRequest.f rootEntrySpan+              , assertEqual "entry data"+                ( Aeson.object+                  [ "service" .= ("Service Entry" :: String)+                  , "sdk"     .= (Aeson.object+                    [ "name"  .= ("haskell.dummy.root.entry" :: String)+                    , "type"  .= ("entry" :: String)+                    ])+                  ]+                )+                (TraceRequest.spanData rootEntrySpan)+              , assertBool "exit timestamp" $ TraceRequest.ts exitSpan > 0+              , assertBool "exit duration" $ TraceRequest.d exitSpan > 0+              , assertEqual "exit kind" 2 (TraceRequest.k exitSpan)+              , assertEqual "exit error" 0 (TraceRequest.ec exitSpan)+              , assertEqual "exit from" from $ TraceRequest.f exitSpan+              , assertEqual "exit data"+                ( Aeson.object+                  [ "service" .= ("Service Exit" :: String)+                  , "sdk"    .= (Aeson.object+                    [ "name" .= ("haskell.dummy.exit" :: String)+                    , "type" .= ("exit" :: String)+                    ])+                  ]+                )+                (TraceRequest.spanData exitSpan)+              ]+++createSpansWithServiceName :: IO String+createSpansWithServiceName = do+  response <- HttpHelper.doAppRequest "bracket/api/with-service-name" "POST" []   return $ LBSC8.unpack $ HTTP.responseBody response 
test/integration/Instana/SDK/IntegrationTest/Connection.hs view
@@ -59,7 +59,7 @@     recordSpan "haskell.dummy.connectionloss.entry-3"     -- wait for span 3 to arrive, check that 1 and 3 have been received -    spansResult <- TestHelper.waitForSpansMatching+    spansResult <- TestHelper.waitForSdkSpansMatching       [ "haskell.dummy.connectionloss.entry-3"       ] @@ -71,11 +71,11 @@       Right spans -> do         let           maybeSpan1 =-            TestHelper.getSpanByName+            TestHelper.getSpanBySdkName               "haskell.dummy.connectionloss.entry-1"               spans           maybeSpan3 =-            TestHelper.getSpanByName+            TestHelper.getSpanBySdkName               "haskell.dummy.connectionloss.entry-3"               spans         if isNothing maybeSpan1@@ -153,13 +153,13 @@           failure     (Right spansBefore, Right spansAfter) -> do       let-        maybeSpanBefore = TestHelper.getSpanByName+        maybeSpanBefore = TestHelper.getSpanBySdkName           "haskell.agent-restart.before-restart"           spansBefore-        maybeSpanAfter1 = TestHelper.getSpanByName+        maybeSpanAfter1 = TestHelper.getSpanBySdkName           "haskell.agent-restart.after-restart-1"           spansAfter-        maybeSpanAfter2 = TestHelper.getSpanByName+        maybeSpanAfter2 = TestHelper.getSpanBySdkName           "haskell.agent-restart.after-restart-2"           spansAfter       assertAllIO@@ -176,7 +176,7 @@ shouldUseTranslatedPid pid = do   applyLabel "shouldUseTranslatedPid" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (_, spansResult) <-       TestHelper.withSpanCreation         (recordSpan "haskell.test.pid-translation")@@ -187,7 +187,7 @@       Right spans -> do         let           maybeEntrySpan =-            TestHelper.getSpanByName "haskell.test.pid-translation" spans+            TestHelper.getSpanBySdkName "haskell.test.pid-translation" spans         if isNothing maybeEntrySpan           then             failIO "expected span has not been recorded"
test/integration/Instana/SDK/IntegrationTest/HttpTracing.hs view
@@ -88,9 +88,9 @@     HttpHelper.doAppRequest urlPath "GET" headers   let     result = LBSC8.unpack $ HTTP.responseBody response-    from = Just $ From pid+    from = Just $ From pid "agent-stub-id"   spansResults <--    TestHelper.waitForSpansMatching+    TestHelper.waitForRegisteredSpansMatching       [ "haskell.wai.server", "haskell.http.client" ]   case spansResults of     Left failure ->@@ -98,8 +98,9 @@     Right spans -> do       let         maybeEntrySpan =-          TestHelper.getSpanByName "haskell.wai.server" spans-        maybeExitSpan = TestHelper.getSpanByName "haskell.http.client" spans+          TestHelper.getSpanByRegisteredName "haskell.wai.server" spans+        maybeExitSpan =+          TestHelper.getSpanByRegisteredName "haskell.http.client" spans       if isNothing maybeEntrySpan || isNothing maybeExitSpan         then           failIO "expected spans have not been recorded"@@ -121,7 +122,7 @@   -- wait a second, then check that no spans have been recorded   threadDelay $ 10 * 1000   spansResults <--    TestHelper.waitForSpansMatching []+    TestHelper.waitForRegisteredSpansMatching []   case spansResults of     Left failure ->       failIO $ "Could not load recorded spans from agent stub: " ++ failure
test/integration/Instana/SDK/IntegrationTest/LowLevelApi.hs view
@@ -2,7 +2,7 @@ module Instana.SDK.IntegrationTest.LowLevelApi   ( shouldRecordSpans   , shouldRecordNonRootEntry-  , shouldMergeData+  , shouldMergeTags   ) where  @@ -25,7 +25,7 @@ shouldRecordSpans pid =   applyLabel "shouldRecordSpans" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (result, spansResults) <-       TestHelper.withSpanCreation         createRootEntry@@ -38,8 +38,8 @@       Right spans -> do         let           maybeRootEntrySpan =-            TestHelper.getSpanByName "haskell.dummy.root.entry" spans-          maybeExitSpan = TestHelper.getSpanByName "haskell.dummy.exit" spans+            TestHelper.getSpanBySdkName "haskell.dummy.root.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans         if isNothing maybeRootEntrySpan || isNothing maybeExitSpan           then             failIO "expected spans have not been recorded"@@ -87,7 +87,7 @@ shouldRecordNonRootEntry pid =   applyLabel "shouldRecordNonRootEntry" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (result, spansResults) <-       TestHelper.withSpanCreation         createNonRootEntry@@ -100,8 +100,8 @@       Right spans -> do         let           maybeEntrySpan =-            TestHelper.getSpanByName "haskell.dummy.entry" spans-          maybeExitSpan = TestHelper.getSpanByName "haskell.dummy.exit" spans+            TestHelper.getSpanBySdkName "haskell.dummy.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans         if isNothing maybeEntrySpan || isNothing maybeExitSpan           then             failIO "expected spans have not been recorded"@@ -143,14 +143,14 @@   return $ LBSC8.unpack $ HTTP.responseBody response  -shouldMergeData :: String -> IO Test-shouldMergeData pid =-  applyLabel "shouldMergeData" $ do+shouldMergeTags :: String -> IO Test+shouldMergeTags pid =+  applyLabel "shouldMergeTags" $ do     let-      from = Just $ From pid+      from = Just $ From pid "agent-stub-id"     (result, spansResults) <-       TestHelper.withSpanCreation-        createSpansWithData+        createSpansWithTags         [ "haskell.dummy.root.entry"         , "haskell.dummy.exit"         ]@@ -160,8 +160,8 @@       Right spans -> do         let           maybeRootEntrySpan =-            TestHelper.getSpanByName "haskell.dummy.root.entry" spans-          maybeExitSpan = TestHelper.getSpanByName "haskell.dummy.exit" spans+            TestHelper.getSpanBySdkName "haskell.dummy.root.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans         if isNothing maybeRootEntrySpan || isNothing maybeExitSpan           then             failIO "expected spans have not been recorded"@@ -189,17 +189,24 @@               , assertEqual "entry from" from $ TraceRequest.f rootEntrySpan               , assertEqual "entry data"                 ( Aeson.object-                  [ "data1"     .= ("value1" :: String)-                  , "data2"     .= (1302 :: Int)-                  , "startKind" .= ("entry" :: String)-                  , "data2"     .= (1302 :: Int)-                  , "data3"     .= ("value3" :: String)-                  , "nested"    .= (Aeson.object [-                      "entry" .= (Aeson.object [-                        "key" .= ("nested.entry.value" :: String)+                  [ "sdk"    .= (Aeson.object+                    [ "name" .= ("haskell.dummy.root.entry" :: String)+                    , "type" .= ("entry" :: String)+                    , "custom" .= (Aeson.object+                      [ "tags" .= (Aeson.object+                        [ "startKind" .= ("entry" :: String)+                        , "endKind"   .= ("entry" :: String)+                        , "data1"     .= ("value1" :: String)+                        , "data2"     .= (1302 :: Int)+                        , "data3"     .= ("value3" :: String)+                        , "nested"    .= (Aeson.object [+                            "entry" .= (Aeson.object [+                              "key" .= ("nested.entry.value" :: String)+                            ])+                          ])+                        ])                       ])                     ])-                  , "endKind"   .= ("entry" :: String)                   ]                 )                 (TraceRequest.spanData rootEntrySpan)@@ -210,25 +217,32 @@               , assertEqual "exit from" from $ TraceRequest.f exitSpan               , assertEqual "exit data"                 ( Aeson.object-                  [ "data1"     .= ("value1" :: String)-                  , "data2"     .= (1302 :: Int)-                  , "startKind" .= ("exit" :: String)-                  , "data2"     .= (1302 :: Int)-                  , "data3"     .= ("value3" :: String)-                  , "nested"    .= (Aeson.object [-                      "exit" .= (Aeson.object [-                        "key" .= ("nested.exit.value" :: String)+                  [ "sdk"    .= (Aeson.object+                    [ "name" .= ("haskell.dummy.exit" :: String)+                    , "type" .= ("exit" :: String)+                    , "custom" .= (Aeson.object+                      [ "tags" .= (Aeson.object+                        [ "startKind" .= ("exit" :: String)+                        , "endKind"   .= ("exit" :: String)+                        , "data1"     .= ("value1" :: String)+                        , "data2"     .= (1302 :: Int)+                        , "data3"     .= ("value3" :: String)+                        , "nested"    .= (Aeson.object+                          [ "exit" .= (Aeson.object+                            [ "key" .= ("nested.exit.value" :: String)+                            ])+                          ])+                        ])                       ])                     ])-                  , "endKind"   .= ("exit" :: String)                   ]                 )                 (TraceRequest.spanData exitSpan)               ]  -createSpansWithData :: IO String-createSpansWithData = do-  response <- HttpHelper.doAppRequest "low/level/api/with-data" "POST" []+createSpansWithTags :: IO String+createSpansWithTags = do+  response <- HttpHelper.doAppRequest "low/level/api/with-tags" "POST" []   return $ LBSC8.unpack $ HTTP.responseBody response 
test/integration/Instana/SDK/IntegrationTest/Metrics.hs view
@@ -51,7 +51,7 @@               (EntityDataRequest.arguments entityData)           , assertLabelIs               "sensorVersion"-              "0.2.0.0"+              "0.3.0.0"               (EntityDataRequest.sensorVersion entityData)           , assertCounterSatisfies               "startTime"
test/integration/Instana/SDK/IntegrationTest/Runner.hs view
@@ -106,6 +106,7 @@       buildCommand         [ ("APP_LOG_LEVEL", Just logLevel)         , ("INSTANA_AGENT_NAME", Suite.customAgentName options)+        , ("INSTANA_SERVICE_NAME", Suite.customServiceName options)         , ("INSTANA_LOG_LEVEL", Just logLevel)         ]         "stack exec " ++ Suite.appUnderTest options
+ test/integration/Instana/SDK/IntegrationTest/ServiceName.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE OverloadedStrings #-}+module Instana.SDK.IntegrationTest.ServiceName+  ( shouldUseServiceNameEnvVar+  ) where+++import           Data.Aeson                             ((.=))+import qualified Data.Aeson                             as Aeson+import           Data.ByteString.Lazy.Char8             as LBSC8+import           Data.Maybe                             (isNothing)+import qualified Network.HTTP.Client                    as HTTP+import           Test.HUnit++import           Instana.SDK.AgentStub.TraceRequest     (From (..))+import qualified Instana.SDK.AgentStub.TraceRequest     as TraceRequest+import qualified Instana.SDK.IntegrationTest.HttpHelper as HttpHelper+import           Instana.SDK.IntegrationTest.HUnitExtra (applyLabel,+                                                         assertAllIO, failIO)+import qualified Instana.SDK.IntegrationTest.TestHelper as TestHelper+++shouldUseServiceNameEnvVar :: String -> IO Test+shouldUseServiceNameEnvVar pid =+  applyLabel "shouldUseServiceNameEnvVar" $ do+    let+      from = Just $ From pid "agent-stub-id"+    (result, spansResults) <-+      TestHelper.withSpanCreation+        createSpansWithServiceName+        [ "haskell.dummy.root.entry"+        , "haskell.dummy.exit"+        ]+    case spansResults of+      Left failure ->+        failIO $ "Could not load recorded spans from agent stub: " ++ failure+      Right spans -> do+        let+          maybeRootEntrySpan =+            TestHelper.getSpanBySdkName "haskell.dummy.root.entry" spans+          maybeExitSpan = TestHelper.getSpanBySdkName "haskell.dummy.exit" spans+        if isNothing maybeRootEntrySpan || isNothing maybeExitSpan+          then+            failIO "expected spans have not been recorded"+          else do+            let+              Just rootEntrySpan = maybeRootEntrySpan+              Just exitSpan = maybeExitSpan+            assertAllIO+              [ assertEqual "result" "exit done::entry done" result+              , assertEqual "trace ID is consistent"+                  (TraceRequest.t rootEntrySpan)+                  (TraceRequest.t exitSpan)+              , assertEqual "root.traceId == root.spanId"+                  (TraceRequest.s rootEntrySpan)+                  (TraceRequest.t rootEntrySpan)+              , assertBool "root has no parent" $+                  isNothing $ TraceRequest.p rootEntrySpan+              , assertEqual "exit parent"+                  (Just $ TraceRequest.s rootEntrySpan)+                  (TraceRequest.p exitSpan)+              , assertBool "entry timestamp" $ TraceRequest.ts rootEntrySpan > 0+              , assertBool "entry duration" $ TraceRequest.d rootEntrySpan > 0+              , assertEqual "entry kind" 1 (TraceRequest.k rootEntrySpan)+              , assertEqual "entry error" 0 (TraceRequest.ec rootEntrySpan)+              , assertEqual "entry from" from $ TraceRequest.f rootEntrySpan+              , assertEqual "entry data"+                ( Aeson.object+                  -- service name from env var:+                  [ "service" .= ("Custom Service Name" :: String)+                  , "sdk"     .= (Aeson.object+                    [ "name"  .= ("haskell.dummy.root.entry" :: String)+                    , "type"  .= ("entry" :: String)+                    ])+                  ]+                )+                (TraceRequest.spanData rootEntrySpan)+              , assertBool "exit timestamp" $ TraceRequest.ts exitSpan > 0+              , assertBool "exit duration" $ TraceRequest.d exitSpan > 0+              , assertEqual "exit kind" 2 (TraceRequest.k exitSpan)+              , assertEqual "exit error" 0 (TraceRequest.ec exitSpan)+              , assertEqual "exit from" from $ TraceRequest.f exitSpan+              , assertEqual "exit data"+                ( Aeson.object+                  -- service name from span tag:+                  [ "service" .= ("Service Exit" :: String)+                  , "sdk"    .= (Aeson.object+                    [ "name" .= ("haskell.dummy.exit" :: String)+                    , "type" .= ("exit" :: String)+                    ])+                  ]+                )+                (TraceRequest.spanData exitSpan)+              ]+++createSpansWithServiceName :: IO String+createSpansWithServiceName = do+  response <-+    HttpHelper.doAppRequest+      "bracket/api/with-service-name-exit-only"+      "POST"+      []+  return $ LBSC8.unpack $ HTTP.responseBody response+
test/integration/Instana/SDK/IntegrationTest/Suite.hs view
@@ -6,6 +6,7 @@   , isExclusive   , withConnectionLoss   , withCustomAgentName+  , withCustomServiceName   , withPidTranslation   , withStartupDelay   ) where@@ -31,6 +32,7 @@     , startupDelay           :: Bool     , simulateConnectionLoss :: Bool     , appUnderTest           :: String+    , customServiceName      :: Maybe String     }  @@ -42,6 +44,7 @@     , startupDelay           = False     , simulateConnectionLoss = False     , appUnderTest           = "instana-haskell-test-wai-server"+    , customServiceName      = Nothing     }  @@ -63,6 +66,11 @@ withConnectionLoss :: SuiteOptions withConnectionLoss =   defaultOptions { simulateConnectionLoss = True }+++withCustomServiceName :: String -> SuiteOptions+withCustomServiceName serviceName =+  defaultOptions { customServiceName = Just serviceName }   data ConditionalSuite =
test/integration/Instana/SDK/IntegrationTest/TestHelper.hs view
@@ -1,7 +1,10 @@ {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE ScopedTypeVariables #-} module Instana.SDK.IntegrationTest.TestHelper-  ( getSpanByName+  ( getSpanByRegisteredName+  , getSpanBySdkName+  , hasRegisteredSpanName+  , hasSdkSpanName   , pingAgentStub   , pingApp   , resetDiscoveries@@ -12,7 +15,8 @@   , waitForExternalAgentConnection   , waitForDiscoveryWithPid   , waitForAgentReadyWithPid-  , waitForSpansMatching+  , waitForRegisteredSpansMatching+  , waitForSdkSpansMatching   , withSpanCreation   ) where @@ -44,7 +48,7 @@   -> IO (a, Either String [Span]) withSpanCreation createSpanAction expectedSpans = do   result <- createSpanAction-  spansResults <- waitForSpansMatching expectedSpans+  spansResults <- waitForSdkSpansMatching expectedSpans   resetSpans   return (result, spansResults) @@ -230,31 +234,68 @@         entityDataRequests  -waitForSpansMatching :: [Text] -> IO (Either String [Span])-waitForSpansMatching expectedNames = do+waitForSdkSpansMatching :: [Text] -> IO (Either String [Span])+waitForSdkSpansMatching expectedNames = do   infoM testLogger "⏱  waiting for spans to be processed"-  spans <- HttpHelper.retryRequest (hasMatchingSpans expectedNames) getSpans+  spans <- HttpHelper.retryRequest (hasMatchingSdkSpans expectedNames) getSpans   infoM testLogger "✅ spans have been processed"   return spans  -hasMatchingSpans :: [Text] -> [Span] -> Bool-hasMatchingSpans expectedNames spans =+hasMatchingSdkSpans :: [Text] -> [Span] -> Bool+hasMatchingSdkSpans expectedNames spans =   let+     namesFromResponse = List.map TraceRequest.readSdkName spans+     justExpectedNames = List.map Just expectedNames+     intersection = List.intersect namesFromResponse justExpectedNames+   in+     length intersection == length expectedNames+++waitForRegisteredSpansMatching :: [Text] -> IO (Either String [Span])+waitForRegisteredSpansMatching expectedNames = do+  infoM testLogger "⏱  waiting for spans to be processed"+  spans <-+    HttpHelper.retryRequest (hasMatchingRegisteredSpans expectedNames) getSpans+  infoM testLogger "✅ spans have been processed"+  return spans+++hasMatchingRegisteredSpans :: [Text] -> [Span] -> Bool+hasMatchingRegisteredSpans expectedNames spans =+  let      namesFromResponse = List.map TraceRequest.n spans      intersection = List.intersect namesFromResponse expectedNames    in      length intersection == length expectedNames  +getSpanByRegisteredName :: Text -> [Span] -> Maybe Span+getSpanByRegisteredName name =+  List.find (hasRegisteredSpanName name)+++hasRegisteredSpanName :: Text -> Span -> Bool+hasRegisteredSpanName name span_ =+   TraceRequest.n span_ == name+++getSpanBySdkName :: Text -> [Span] -> Maybe Span+getSpanBySdkName name =+  List.find (hasSdkSpanName name)+++hasSdkSpanName :: Text -> Span -> Bool+hasSdkSpanName name span_ =+  let+    sdkName = TraceRequest.readSdkName span_+  in+    sdkName == Just name++ getSpans :: IO (Either String [Span]) getSpans =   HttpHelper.requestAgentStubAndParse "stub/spans" "GET"---getSpanByName :: Text -> [Span] -> Maybe Span-getSpanByName name =-  List.find (\s -> TraceRequest.n s == name)   -- |Will also reset agent ready requests and entity data requests (basically
test/integration/Instana/SDK/IntegrationTest/TestSuites.hs view
@@ -6,6 +6,7 @@ import qualified Instana.SDK.IntegrationTest.HttpTracing   as HttpTracing import qualified Instana.SDK.IntegrationTest.LowLevelApi   as LowLevelApi import qualified Instana.SDK.IntegrationTest.Metrics       as Metrics+import qualified Instana.SDK.IntegrationTest.ServiceName   as ServiceName import           Instana.SDK.IntegrationTest.Suite         (ConditionalSuite (..),                                                             Suite (..)) import qualified Instana.SDK.IntegrationTest.Suite         as Suite@@ -21,6 +22,7 @@   , testAgentRestart   , testPidTranslation   , testCustomAgentName+  , testServiceName   , testHttpTracing   , testWaiMiddleware   , testMetrics@@ -35,7 +37,8 @@       , Suite.tests = (\pid ->          [ BracketApi.shouldRecordSpans pid          , BracketApi.shouldRecordNonRootEntry pid-         , BracketApi.shouldMergeData pid+         , BracketApi.shouldMergeTags pid+         , BracketApi.shouldSetServiceName pid          ])       , Suite.options = Suite.defaultOptions       }@@ -49,7 +52,7 @@       , Suite.tests = (\pid ->          [ LowLevelApi.shouldRecordSpans pid          , LowLevelApi.shouldRecordNonRootEntry pid-         , LowLevelApi.shouldMergeData pid+         , LowLevelApi.shouldMergeTags pid          ])       , Suite.options = Suite.defaultOptions       }@@ -100,6 +103,18 @@           Connection.shouldUseTranslatedPid pid         ])       , Suite.options = Suite.withPidTranslation+      }+++testServiceName :: ConditionalSuite+testServiceName =+  Run $+    Suite+      { Suite.label = "INSTANA_SERVICE_NAME env var"+      , Suite.tests = (\pid ->+         [ ServiceName.shouldUseServiceNameEnvVar pid+         ])+      , Suite.options = Suite.withCustomServiceName "Custom Service Name"       }  
test/integration/Instana/SDK/IntegrationTest/WaiMiddleware.hs view
@@ -57,9 +57,9 @@     HttpHelper.doAppRequest urlPath "GET" headers   let     result = LBSC8.unpack $ HTTP.responseBody response-    from = Just $ From pid+    from = Just $ From pid "agent-stub-id"   spansResults <--    TestHelper.waitForSpansMatching+    TestHelper.waitForRegisteredSpansMatching       [ "haskell.wai.server", "haskell.http.client" ]   case spansResults of     Left failure ->@@ -67,8 +67,9 @@     Right spans -> do       let         maybeEntrySpan =-          TestHelper.getSpanByName "haskell.wai.server" spans-        maybeExitSpan = TestHelper.getSpanByName "haskell.http.client" spans+          TestHelper.getSpanByRegisteredName "haskell.wai.server" spans+        maybeExitSpan =+          TestHelper.getSpanByRegisteredName "haskell.http.client" spans       if isNothing maybeEntrySpan || isNothing maybeExitSpan         then           failIO "expected spans have not been recorded"@@ -90,7 +91,7 @@   -- wait a second, then check that no spans have been recorded   threadDelay $ 10 * 1000   spansResults <--    TestHelper.waitForSpansMatching []+    TestHelper.waitForRegisteredSpansMatching []   case spansResults of     Left failure ->       failIO $ "Could not load recorded spans from agent stub: " ++ failure
test/shared/Instana/SDK/AgentStub/TraceRequest.hs view
@@ -4,9 +4,11 @@ module Instana.SDK.AgentStub.TraceRequest where  -import           Data.Aeson   (FromJSON, ToJSON, Value, (.:), (.=))-import qualified Data.Aeson   as Aeson-import           Data.Text    (Text)+import           Data.Aeson          (FromJSON, ToJSON, Value (Object), (.:),+                                      (.=))+import qualified Data.Aeson          as Aeson+import qualified Data.HashMap.Strict as HM+import           Data.Text           (Text) import           GHC.Generics  @@ -14,7 +16,8 @@   data From = From-  { entityId :: String+  { entityId  :: String+  , agentUuid :: String   } deriving (Eq, Generic, Show)  @@ -23,12 +26,15 @@     \fr ->       From         <$> fr .: "e" -- entityId+        <*> fr .: "h" -- agent UUID/host ID   instance ToJSON From where   toJSON :: From -> Value   toJSON fr = Aeson.object-    [ "e" .= entityId fr ]+    [ "e" .= entityId fr+    , "h" .= agentUuid fr+    ]   data Span =@@ -75,4 +81,20 @@     , "data" .= spanData sp     , "f"    .= f sp     ]+++readSdkName :: Span -> Maybe Text+readSdkName span_ =+  let+    value = extractProperty ["sdk", "name"] (spanData span_)+  in+    case value of+      Just (Aeson.String sdkName) -> Just sdkName+      _                           -> Nothing+++extractProperty :: [Text] -> Value -> Maybe Value+extractProperty [] value              = Just value+extractProperty (key:keys) (Object o) = HM.lookup key o >>= extractProperty keys+extractProperty _      _              = Nothing 
test/unit/Instana/SDK/Internal/SpanStackTest.hs view
@@ -296,20 +296,22 @@       { RootEntry.spanAndTraceId = Id.fromString "traceId"       , RootEntry.spanName       = "test.entry"       , RootEntry.timestamp      = 1514761200000-      , RootEntry.spanData       = emptyValue       , RootEntry.errorCount     = 0+      , RootEntry.serviceName    = Nothing+      , RootEntry.spanData       = emptyValue       }   exitSpan :: ExitSpan exitSpan =   ExitSpan-    { ExitSpan.parentSpan = entrySpan-    , ExitSpan.spanId     = Id.fromString "spanId"-    , ExitSpan.spanName   = "test.exit"-    , ExitSpan.timestamp  = 1514761201000-    , ExitSpan.spanData   = emptyValue-    , ExitSpan.errorCount = 0+    { ExitSpan.parentSpan   = entrySpan+    , ExitSpan.spanId      = Id.fromString "spanId"+    , ExitSpan.spanName    = "test.exit"+    , ExitSpan.timestamp   = 1514761201000+    , ExitSpan.errorCount  = 0+    , ExitSpan.serviceName = Nothing+    , ExitSpan.spanData    = emptyValue     }  
test/unit/Instana/SDK/Internal/SpanTest.hs view
@@ -24,14 +24,15 @@     , TestLabel "shouldAddIntDeeplyNested" shouldAddIntDeeplyNested     , TestLabel "shouldAddDoubleDeeplyNested" shouldAddDoubleDeeplyNested     , TestLabel "shouldAddBooleanDeeplyNested" shouldAddBooleanDeeplyNested-    , TestLabel "shouldAddMultiple" shouldAddMultiple+    , TestLabel "shouldAddMultipleRegistered" shouldAddMultipleRegistered+    , TestLabel "shouldAddMultipleTags" shouldAddMultipleTags     ]   shouldAddStringNotNested :: Test shouldAddStringNotNested =   let-    span_ = Span.addDataAt "path" ("value" :: String) $ entrySpan+    span_ = Span.addRegisteredDataAt "path" ("value" :: String) $ entrySpan     spanData = Span.spanData span_   in   TestCase $@@ -46,7 +47,7 @@ shouldAddStringNested =   let     span_ =-      Span.addDataAt+      Span.addRegisteredDataAt         "nested.path"         ("value" :: String) $           entrySpan@@ -66,7 +67,7 @@ shouldAddStringDeeplyNested =   let     span_ =-      Span.addDataAt+      Span.addRegisteredDataAt         "really.deeply.nested.path"         ("deeplyNestedValue" :: String) $           entrySpan@@ -90,7 +91,7 @@ shouldAddIntDeeplyNested =   let     span_ =-      Span.addDataAt+      Span.addRegisteredDataAt         "really.deeply.nested.path"         (42 :: Int) $           entrySpan@@ -114,7 +115,7 @@ shouldAddDoubleDeeplyNested =   let     span_ =-      Span.addDataAt+      Span.addRegisteredDataAt         "really.deeply.nested.path"         (28.08 :: Double) $           entrySpan@@ -137,7 +138,7 @@ shouldAddBooleanDeeplyNested :: Test shouldAddBooleanDeeplyNested =   let-    span_ = Span.addDataAt "really.deeply.nested.path" True $ entrySpan+    span_ = Span.addRegisteredDataAt "really.deeply.nested.path" True $ entrySpan     spanData = Span.spanData span_   in   TestCase $@@ -154,16 +155,16 @@       spanData  -shouldAddMultiple :: Test-shouldAddMultiple =+shouldAddMultipleRegistered :: Test+shouldAddMultipleRegistered =   let     span_ =-      Span.addDataAt "nested.key3" (12.07 :: Double) $-      Span.addDataAt "nested.key2" (2 :: Int) $-      Span.addDataAt "nested.key1" ("value.n.1" :: String) $-      Span.addDataAt "key3" (16.04 :: Double) $-      Span.addDataAt "key2" (13 :: Int) $-      Span.addDataAt "key1" ("value1" :: String) $+      Span.addRegisteredDataAt "nested.key3" (12.07 :: Double) $+      Span.addRegisteredDataAt "nested.key2" (2 :: Int) $+      Span.addRegisteredDataAt "nested.key1" ("value.n.1" :: String) $+      Span.addRegisteredDataAt "key3" (16.04 :: Double) $+      Span.addRegisteredDataAt "key2" (13 :: Int) $+      Span.addRegisteredDataAt "key1" ("value1" :: String) $       entrySpan     spanData = Span.spanData span_   in@@ -183,6 +184,41 @@       spanData  +shouldAddMultipleTags :: Test+shouldAddMultipleTags =+  let+    span_ =+      Span.addTagAt "nested.key3" (12.07 :: Double) $+      Span.addTagAt "nested.key2" (2 :: Int) $+      Span.addTagAt "nested.key1" ("value.n.1" :: String) $+      Span.addTagAt "key3" (16.04 :: Double) $+      Span.addTagAt "key2" (13 :: Int) $+      Span.addTagAt "key1" ("value1" :: String) $+      entrySpan+    spanData = Span.spanData span_+  in+  TestCase $+    assertEqual "add deeply nested"+      (Aeson.object+        [ "sdk" .= (Aeson.object+          [ "custom" .= (Aeson.object+            [ "tags" .= (Aeson.object+              [ "key1" .= ("value1" :: String)+              , "key2" .= (13 :: Int)+              , "key3" .= (16.04 :: Double)+              , "nested" .= (Aeson.object+                [ "key1" .= ("value.n.1" :: String)+                , "key2" .= (2 :: Int)+                , "key3" .= (12.07 :: Double)+                ])+              ])+            ])+          ])+        ]+      )+      spanData++ entrySpan :: Span entrySpan =   Entry $@@ -191,8 +227,9 @@         { RootEntry.spanAndTraceId = Id.fromString "traceId"         , RootEntry.spanName       = "test.entry"         , RootEntry.timestamp      = 1514761200000-        , RootEntry.spanData       = initialData+        , RootEntry.serviceName    = Nothing         , RootEntry.errorCount     = 0+        , RootEntry.spanData       = initialData         }