packages feed

hs-opentelemetry-sdk-1.0.0.0: test/OpenTelemetry/LogSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module OpenTelemetry.LogSpec where

import Control.Monad (replicateM_)
import qualified Data.HashMap.Strict as H
import Data.IORef
import qualified Data.Text as T
import qualified Data.Vector as V
import OpenTelemetry.Attributes (AttributeLimits (..), defaultAttributeLimits, emptyAttributes)
import OpenTelemetry.Exporter.LogRecord (LogRecordExporterArguments (..), mkLogRecordExporter)
import OpenTelemetry.Internal.Common.Types (ExportResult (..), FlushResult (..), InstrumentationLibrary (..), ShutdownResult (..))
import OpenTelemetry.Internal.Log.Types
import OpenTelemetry.Log.Core
import OpenTelemetry.LogAttributes (AnyValue (..), ToValue (..))
import qualified OpenTelemetry.LogAttributes as LA
import OpenTelemetry.Processor.Batch.LogRecord (BatchLogRecordProcessorConfig (..), batchLogRecordProcessor, defaultBatchLogRecordProcessorConfig)
import OpenTelemetry.Resource (emptyMaterializedResources)
import System.Mem.StableName (eqStableName, makeStableName)
import Test.Hspec


spec :: Spec
spec = describe "OpenTelemetry.Log (SDK)" $ do
  -- Logs SDK
  -- https://opentelemetry.io/docs/specs/otel/logs/sdk/
  describe "getLogger scope caching" $ do
    it "getLogger returns same Logger for same scope" $ do
      -- OTel Logs SDK §LoggerProvider: "Implementations SHOULD return the same
      -- Logger instance when called multiple times with the same [scope identity]."
      -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#loggerprovider
      processor <- captureProcessor
      lp <-
        createLoggerProvider [processor] $
          LoggerProviderOptions
            { loggerProviderOptionsResource = emptyMaterializedResources
            , loggerProviderOptionsAttributeLimits = defaultAttributeLimits
            , loggerProviderOptionsMinSeverity = Nothing
            }
      let scope = instrumentationLibrary "test-lib" "1.0"
      l1 <- getLogger lp scope
      l2 <- getLogger lp scope
      loggerInstrumentationScope l1 `shouldBe` loggerInstrumentationScope l2
      sn1 <- makeStableName l1
      sn2 <- makeStableName l2
      eqStableName sn1 sn2 `shouldBe` True
      let scope2 = instrumentationLibrary "other-lib" "2.0"
      l3 <- getLogger lp scope2
      loggerInstrumentationScope l3 `shouldNotBe` loggerInstrumentationScope l1
      sn3 <- makeStableName l3
      eqStableName sn1 sn3 `shouldBe` False

  describe "LogRecord attribute limits" $ do
    -- Logs SDK §LogRecord limits
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits
    specify "attributes within count limit are preserved" $ do
      let limits = AttributeLimits {attributeCountLimit = Just 3, attributeLengthLimit = Nothing}
      processor <- captureProcessor
      lp <-
        createLoggerProvider [processor] $
          LoggerProviderOptions
            { loggerProviderOptionsResource = emptyMaterializedResources
            , loggerProviderOptionsAttributeLimits = limits
            , loggerProviderOptionsMinSeverity = Nothing
            }
      let logger = makeLogger lp testLib
      lr <-
        emitLogRecord logger $
          emptyLogRecordArguments
            { attributes = H.fromList [("a", toValue ("1" :: T.Text)), ("b", toValue ("2" :: T.Text))]
            , severityNumber = Just Info
            }
      ilr <- readLogRecord lr
      LA.attributesCount (logRecordAttributes ilr) `shouldBe` 2
      LA.attributesDropped (logRecordAttributes ilr) `shouldBe` 0

    -- Logs SDK §LogRecord limits: dropped attributes
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits
    specify "attributes exceeding count limit are dropped" $ do
      let limits = AttributeLimits {attributeCountLimit = Just 2, attributeLengthLimit = Nothing}
      processor <- captureProcessor
      lp <-
        createLoggerProvider [processor] $
          LoggerProviderOptions
            { loggerProviderOptionsResource = emptyMaterializedResources
            , loggerProviderOptionsAttributeLimits = limits
            , loggerProviderOptionsMinSeverity = Nothing
            }
      let logger = makeLogger lp testLib
      lr <-
        emitLogRecord logger $
          emptyLogRecordArguments
            { attributes = H.fromList [("a", toValue ("1" :: T.Text)), ("b", toValue ("2" :: T.Text)), ("c", toValue ("3" :: T.Text))]
            , severityNumber = Just Info
            }
      ilr <- readLogRecord lr
      LA.attributesDropped (logRecordAttributes ilr) `shouldSatisfy` (> 0)

    -- Logs SDK §LogRecord limits: attribute value length
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits
    specify "length limit truncates log record attribute values" $ do
      let limits = AttributeLimits {attributeCountLimit = Nothing, attributeLengthLimit = Just 5}
      processor <- captureProcessor
      lp <-
        createLoggerProvider [processor] $
          LoggerProviderOptions
            { loggerProviderOptionsResource = emptyMaterializedResources
            , loggerProviderOptionsAttributeLimits = limits
            , loggerProviderOptionsMinSeverity = Nothing
            }
      let logger = makeLogger lp testLib
      lr <-
        emitLogRecord logger $
          emptyLogRecordArguments
            { attributes = H.fromList [("key", toValue ("hello world" :: T.Text))]
            , severityNumber = Just Info
            }
      ilr <- readLogRecord lr
      LA.lookupAttribute (logRecordAttributes ilr) "key" `shouldBe` Just (TextValue "hello")

    -- Logs SDK §LogRecord limits: unset limits
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#logrecord-limits
    specify "no limit allows many attributes" $ do
      let limits = AttributeLimits {attributeCountLimit = Nothing, attributeLengthLimit = Nothing}
      processor <- captureProcessor
      lp <-
        createLoggerProvider [processor] $
          LoggerProviderOptions
            { loggerProviderOptionsResource = emptyMaterializedResources
            , loggerProviderOptionsAttributeLimits = limits
            , loggerProviderOptionsMinSeverity = Nothing
            }
      let logger = makeLogger lp testLib
          attrs = H.fromList [(T.pack ("k" ++ show i), toValue (T.pack (show i))) | i <- [1 :: Int .. 200]]
      lr <-
        emitLogRecord logger $
          emptyLogRecordArguments
            { attributes = attrs
            , severityNumber = Just Info
            }
      ilr <- readLogRecord lr
      LA.attributesCount (logRecordAttributes ilr) `shouldBe` 200
      LA.attributesDropped (logRecordAttributes ilr) `shouldBe` 0

  describe "Batch LogRecord Processor" $ do
    -- Logs SDK §BatchLogRecordProcessor: shutdown flushes pending records
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#batchlogrecordprocessor
    specify "shutdown exports buffered records" $ do
      exportedRef <- newIORef (0 :: Int)
      exporter <-
        mkLogRecordExporter
          LogRecordExporterArguments
            { logRecordExporterArgumentsExport = \batch -> do
                atomicModifyIORef' exportedRef (\n -> (n + V.length batch, ()))
                pure Success
            , logRecordExporterArgumentsForceFlush = pure FlushSuccess
            , logRecordExporterArgumentsShutdown = pure ()
            }
      let conf =
            (defaultBatchLogRecordProcessorConfig exporter)
              { batchLogScheduledDelayMillis = 60000
              , batchLogMaxQueueSize = 100
              }
      proc <- batchLogRecordProcessor conf
      lp <- createLoggerProvider [proc] emptyLoggerProviderOptions
      let logger = makeLogger lp testLib
      replicateM_ 5 $ emitLogRecord logger emptyLogRecordArguments {severityNumber = Just Info}
      _ <- shutdownLoggerProvider lp Nothing
      exported <- readIORef exportedRef
      exported `shouldBe` 5

    -- Logs SDK §BatchLogRecordProcessor: ForceFlush exports pending records
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#batchlogrecordprocessor
    specify "forceFlush exports buffered records" $ do
      exportedRef <- newIORef (0 :: Int)
      exporter <-
        mkLogRecordExporter
          LogRecordExporterArguments
            { logRecordExporterArgumentsExport = \batch -> do
                atomicModifyIORef' exportedRef (\n -> (n + V.length batch, ()))
                pure Success
            , logRecordExporterArgumentsForceFlush = pure FlushSuccess
            , logRecordExporterArgumentsShutdown = pure ()
            }
      let conf =
            (defaultBatchLogRecordProcessorConfig exporter)
              { batchLogScheduledDelayMillis = 60000
              , batchLogMaxQueueSize = 100
              }
      proc <- batchLogRecordProcessor conf
      lp <- createLoggerProvider [proc] emptyLoggerProviderOptions
      let logger = makeLogger lp testLib
      replicateM_ 3 $ emitLogRecord logger emptyLogRecordArguments {severityNumber = Just Warn}
      _ <- forceFlushLoggerProvider lp Nothing
      exported <- readIORef exportedRef
      exported `shouldBe` 3

    -- Logs SDK §LoggerProvider: Shutdown is idempotent / second shutdown fails
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#shutdown
    specify "shutdown after shutdown returns failure" $ do
      exporter <-
        mkLogRecordExporter
          LogRecordExporterArguments
            { logRecordExporterArgumentsExport = \_ -> pure Success
            , logRecordExporterArgumentsForceFlush = pure FlushSuccess
            , logRecordExporterArgumentsShutdown = pure ()
            }
      proc <- batchLogRecordProcessor (defaultBatchLogRecordProcessorConfig exporter)
      lp <- createLoggerProvider [proc] emptyLoggerProviderOptions
      r1 <- shutdownLoggerProvider lp Nothing
      r1 `shouldBe` ShutdownSuccess
      r2 <- shutdownLoggerProvider lp Nothing
      r2 `shouldBe` ShutdownFailure

    -- Logs SDK §LoggerProvider: no telemetry after Shutdown
    -- https://opentelemetry.io/docs/specs/otel/logs/sdk/#shutdown
    specify "records emitted after shutdown are silently dropped" $ do
      exportedRef <- newIORef (0 :: Int)
      exporter <-
        mkLogRecordExporter
          LogRecordExporterArguments
            { logRecordExporterArgumentsExport = \batch -> do
                atomicModifyIORef' exportedRef (\n -> (n + V.length batch, ()))
                pure Success
            , logRecordExporterArgumentsForceFlush = pure FlushSuccess
            , logRecordExporterArgumentsShutdown = pure ()
            }
      proc <- batchLogRecordProcessor (defaultBatchLogRecordProcessorConfig exporter)
      lp <- createLoggerProvider [proc] emptyLoggerProviderOptions
      let logger = makeLogger lp testLib
      _ <- emitLogRecord logger emptyLogRecordArguments {severityNumber = Just Info}
      _ <- shutdownLoggerProvider lp Nothing
      beforeCount <- readIORef exportedRef
      _ <- emitLogRecord logger emptyLogRecordArguments {severityNumber = Just Info}
      afterCount <- readIORef exportedRef
      afterCount `shouldBe` beforeCount


testLib :: InstrumentationLibrary
testLib =
  InstrumentationLibrary
    { libraryName = "test"
    , libraryVersion = "0.0.0"
    , librarySchemaUrl = ""
    , libraryAttributes = emptyAttributes
    }


captureProcessor :: IO LogRecordProcessor
captureProcessor = do
  ref <- newIORef ([] :: [ReadWriteLogRecord])
  pure
    LogRecordProcessor
      { logRecordProcessorOnEmit = \lr _ctx -> atomicModifyIORef' ref (\rs -> (lr : rs, ()))
      , logRecordProcessorShutdown = pure ShutdownSuccess
      , logRecordProcessorForceFlush = pure FlushSuccess
      }