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 +13/−0
- ChangeLog.md +34/−2
- README.md +22/−7
- instana-haskell-trace-sdk.cabal +5/−2
- src/Instana/SDK/Config.hs +4/−0
- src/Instana/SDK/Internal/Config.hs +15/−0
- src/Instana/SDK/Internal/Context.hs +2/−2
- src/Instana/SDK/Internal/FullSpan.hs +0/−145
- src/Instana/SDK/Internal/WireSpan.hs +144/−0
- src/Instana/SDK/Internal/Worker.hs +42/−34
- src/Instana/SDK/SDK.hs +113/−53
- src/Instana/SDK/Span/EntrySpan.hs +20/−0
- src/Instana/SDK/Span/ExitSpan.hs +18/−8
- src/Instana/SDK/Span/NonRootEntry.hs +19/−9
- src/Instana/SDK/Span/RootEntry.hs +12/−1
- src/Instana/SDK/Span/Span.hs +52/−12
- src/Instana/SDK/Span/SpanType.hs +90/−0
- test/apps/wai/Main.hs +92/−32
- test/integration/Instana/SDK/IntegrationTest/BracketApi.hs +137/−43
- test/integration/Instana/SDK/IntegrationTest/Connection.hs +8/−8
- test/integration/Instana/SDK/IntegrationTest/HttpTracing.hs +6/−5
- test/integration/Instana/SDK/IntegrationTest/LowLevelApi.hs +49/−35
- test/integration/Instana/SDK/IntegrationTest/Metrics.hs +1/−1
- test/integration/Instana/SDK/IntegrationTest/Runner.hs +1/−0
- test/integration/Instana/SDK/IntegrationTest/ServiceName.hs +104/−0
- test/integration/Instana/SDK/IntegrationTest/Suite.hs +8/−0
- test/integration/Instana/SDK/IntegrationTest/TestHelper.hs +54/−13
- test/integration/Instana/SDK/IntegrationTest/TestSuites.hs +17/−2
- test/integration/Instana/SDK/IntegrationTest/WaiMiddleware.hs +6/−5
- test/shared/Instana/SDK/AgentStub/TraceRequest.hs +27/−5
- test/unit/Instana/SDK/Internal/SpanStackTest.hs +9/−7
- test/unit/Instana/SDK/Internal/SpanTest.hs +53/−16
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
@@ -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 }