otel-effectful-1.0.0: src/Effectful/OpenTelemetry/Tracing/Span.hs
module Effectful.OpenTelemetry.Tracing.Span where
import Data.Aeson.Types (Pair, ToJSON (..), Value, object, (.=))
import Data.Function ((&))
import Data.Maybe (catMaybes)
import Data.Sequence (Seq)
import Data.Text (Text)
import Data.Text qualified as Text
import Effectful.OpenTelemetry.Protocol.Attributes (Attributes)
import Effectful.OpenTelemetry.Protocol.Export qualified as Export
import Effectful.OpenTelemetry.Protocol.Resource qualified as Resource
import Effectful.OpenTelemetry.Protocol.Scope qualified as Scope
import Effectful.OpenTelemetry.Timestamp (Timestamp (..))
import Effectful.OpenTelemetry.Tracing.Span.Context (Context (..))
import Effectful.OpenTelemetry.Tracing.Span.Event (Event)
import Effectful.OpenTelemetry.Tracing.Span.ID qualified as Span (ID)
import Effectful.OpenTelemetry.Tracing.Span.Kind (Kind (..))
import Effectful.OpenTelemetry.Tracing.Span.Status
import GHC.Generics (Generic)
import Network.GRPC.HTTP2.Proto3Wire (RPC (..))
import Prettyprinter (Pretty (..))
import Prettyprinter.Extra (PrettyAnn (..))
import Prettyprinter.Extra qualified as Pretty
import Prettyprinter.Render.Terminal (AnsiStyle)
import Proto3.Wire.Encode.Class qualified as Proto
import Prelude
-- | A single unit of work or operation in a trace. Spans are the building blocks of traces.
--
-- See <https://opentelemetry.io/docs/concepts/signals/traces/#spans the OpenTelemetry spec>.
data Span = Span
{ name :: Text
, parentSpanId :: Maybe Span.ID
, context :: Context
, kind :: Kind
, startTime :: Timestamp
, endTime :: Maybe Timestamp
, attributes :: Attributes
, events :: Seq Event
, status :: Status
}
deriving stock (Generic, Eq, Show)
instance ToJSON Span where
toJSON Span{context = Context{..}, ..} =
object $
[ "traceId" .= traceId
, "spanId" .= spanId
, "traceState" .= traceState
, "flags" .= traceFlags
, "name" .= name
, "kind" .= kind
, "startTimeUnixNano" .= startTime
, "attributes" .= attributes
, "events" .= events
, "status" .= status
]
<> catMaybes @Pair
[ ("parentSpanId" .=) <$> parentSpanId
, ("endTimeUnixNano" .=) <$> endTime
]
instance Proto.Encode Span where
encode Span{context = Context{..}, ..} =
mconcat
[ Proto.encodeField 1 traceId
, Proto.encodeField 2 spanId
, Proto.encodeField 3 traceState
, foldMap (Proto.encodeField 4) parentSpanId
, Proto.encodeField 5 name
, Proto.encodeField 6 kind
, Proto.encodeField 7 startTime
, foldMap (Proto.encodeField 8) endTime
, Proto.encodeField 9 attributes
, -- 10: dropped_attributes_count: Not supported
Proto.encodeField 11 events
, -- 12: dropped_events_count: Not supported
-- TODO: 13: links
-- 14: dropped_links_count: Not supported
Proto.encodeField 15 status
, Proto.encodeField 16 traceFlags
]
instance PrettyAnn AnsiStyle Span where
prettyAnn Span{..} =
Pretty.unwords . filter (not . Pretty.null) $
[ prettyAnn startTime
, endTime & maybe mempty \(nanos -> subtract startTime.nanos -> duration) ->
pretty . (<> "ms") . Text.show $ duration `div` 1_000_000
, pretty name
, prettyAnn attributes
, prettyAnn status
]
instance Export.Request Span where
exportHttpPathComponents = ["v1", "traces"]
exportGrpcRPC =
RPC
{ pkg = "opentelemetry.proto.collector.trace.v1"
, srv = "TraceService"
, meth = "Export"
}
exportJson = object . pure . ("resourceSpans" .=) . fmap resourceSpans
where
resourceSpans :: Resource.Items Span -> Value
resourceSpans Resource.Items{resource, scopeItems} =
object
[ "resource" .= resource
, "scopeSpans" .= fmap scopeSpans scopeItems
]
scopeSpans :: Scope.Items Span -> Value
scopeSpans Scope.Items{..} =
object
[ "scope" .= scope
, "spans" .= items
]
transportSignalEnvName = "TRACES"
batchEnvPrefix = Just "BSP"
-- https://opentelemetry.io/docs/specs/otel/trace/sdk/#batching-processor
defaultConfig =
Export.Config
{ batch =
Just
Export.BatchConfig
{ maxQueueSize = 2_048
, scheduledDelayMs = 5_000
, maxBatchSize = 512
}
, exportTimeoutMs = 30_000
}