packages feed

otel-effectful-1.0.0: src/Effectful/OpenTelemetry/Logging/LogRecord.hs

module Effectful.OpenTelemetry.Logging.LogRecord where

import Data.Aeson.Types (ToJSON (..), Value (..), object, (.=))
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Maybe (catMaybes)
import Data.Text (Text)
import Effectful.OpenTelemetry.Logging.Severity (Severity)
import Effectful.OpenTelemetry.Protocol (AnyValue (..), 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 qualified as Span
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 hiding (String)

-- | Represents the recording of an event.
--
-- See <https://opentelemetry.io/docs/concepts/signals/logs/#log-record the OpenTelemetry spec>.
data LogRecord = LogRecord
    { timestamp :: Timestamp
    -- ^ Time when the event occured.
    , observedTimestamp :: Timestamp
    -- ^ Time when the event was observed by the collection system.
    , context :: Maybe Span.Context
    -- ^ The tracing context, present if recorded within a span.
    , severity :: Severity
    -- ^ Also known as the log level.
    , body :: Value
    -- ^ Describes the event as structured data.
    -- For a human-readable free-form message, use the 'String' constructor.
    , attributes :: Attributes
    -- ^ Additional information about the event.
    , eventName :: Maybe Text
    -- ^ Identifies the class or type of the event.
    }
    deriving stock (Generic, Eq, Show)

instance ToJSON LogRecord where
    toJSON LogRecord{..} =
        object $
            [ "timeUnixNano" .= timestamp
            , "observedTimeUnixNano" .= observedTimestamp
            , "severityNumber" .= (Number . fromIntegral . fromEnum) severity
            , "body" .= AnyValue body
            , "attributes" .= attributes
            ]
                <> catMaybes
                    [ ("flags" .=) <$> (context <&> (.traceFlags))
                    , ("traceId" .=) <$> (context <&> (.traceId))
                    , ("spanId" .=) <$> (context <&> (.spanId))
                    , ("eventName" .=) <$> eventName
                    ]

instance Proto.Encode LogRecord where
    encode LogRecord{..} =
        mconcat
            [ Proto.encodeField 1 timestamp
            , Proto.encodeField 11 observedTimestamp
            , Proto.encodeField 2 severity
            , -- 3: severity_text: not needed
              Proto.encodeField 5 body
            , Proto.encodeField 6 attributes
            , -- 7: dropped_attributes_count: Not supported
              flip foldMap context \Span.Context{..} ->
                mconcat
                    [ Proto.encodeField 8 traceFlags
                    , Proto.encodeField 9 traceId
                    , Proto.encodeField 10 spanId
                    ]
            , foldMap (Proto.encodeField 12) eventName
            ]

instance PrettyAnn AnsiStyle LogRecord where
    prettyAnn LogRecord{..} =
        Pretty.unwords . filter (not . Pretty.null) $
            [ prettyAnn timestamp
            , prettyAnn severity
            , eventName & maybe mempty \n -> pretty $ "[" <> n <> "]"
            , prettyAnn body
            , prettyAnn attributes
            ]

instance Export.Request LogRecord where
    exportHttpPathComponents = ["v1", "logs"]
    exportGrpcRPC =
        RPC
            { pkg = "opentelemetry.proto.collector.logs.v1"
            , srv = "LogsService"
            , meth = "Export"
            }
    exportJson = object . pure . ("resourceLogs" .=) . fmap resourceLogs
      where
        resourceLogs :: Resource.Items LogRecord -> Value
        resourceLogs Resource.Items{resource, scopeItems} =
            object
                [ "resource" .= resource
                , "scopeLogs" .= fmap scopeLogs scopeItems
                ]
        scopeLogs :: Scope.Items LogRecord -> Value
        scopeLogs Scope.Items{..} =
            object
                [ "scope" .= scope
                , "logRecords" .= items
                ]

    transportSignalEnvName = "LOGS"
    batchEnvPrefix = Just "BLRP"

    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#batching-processor
    defaultConfig =
        Export.Config
            { batch =
                Just
                    Export.BatchConfig
                        { maxQueueSize = 2_048
                        , scheduledDelayMs = 1_000
                        , maxBatchSize = 512
                        }
            , exportTimeoutMs = 30_000
            }