packages feed

hs-opentelemetry-sdk-1.0.0.0: src/OpenTelemetry/Processor/Batch/LogRecord.hs

{-# LANGUAGE RecordWildCards #-}

module OpenTelemetry.Processor.Batch.LogRecord (
  BatchLogRecordProcessorConfig (..),
  defaultBatchLogRecordProcessorConfig,
  batchLogRecordProcessor,
) where

import Control.Concurrent (rtsSupportsBoundThreads)
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Data.IORef (IORef, atomicModifyIORef', newIORef)
import Data.Vector (Vector)
import qualified Data.Vector as V
import OpenTelemetry.Internal.Common.Types (ExportResult (..), ShutdownResult (..))
import OpenTelemetry.Internal.Log.Types
import OpenTelemetry.Internal.Logging (otelLogWarning)
import System.Timeout (timeout)
import VectorBuilder.Builder as Builder
import VectorBuilder.Vector as Builder


data BatchLogRecordProcessorConfig = BatchLogRecordProcessorConfig
  { batchLogExporter :: !LogRecordExporter
  , batchLogMaxQueueSize :: !Int
  , batchLogScheduledDelayMillis :: !Int
  , batchLogExportTimeoutMillis :: !Int
  , batchLogMaxExportBatchSize :: !Int
  }


defaultBatchLogRecordProcessorConfig :: LogRecordExporter -> BatchLogRecordProcessorConfig
defaultBatchLogRecordProcessorConfig e =
  BatchLogRecordProcessorConfig
    { batchLogExporter = e
    , batchLogMaxQueueSize = 2048
    , batchLogScheduledDelayMillis = 1000
    , batchLogExportTimeoutMillis = 30000
    , batchLogMaxExportBatchSize = 512
    }


data BoundedBuffer = BoundedBuffer
  { bufBounds :: !Int
  , bufExportBounds :: !Int
  , bufCount :: !Int
  , bufItems :: !(Builder.Builder ReadableLogRecord)
  }


emptyBuffer :: Int -> Int -> BoundedBuffer
emptyBuffer bounds exportBounds = BoundedBuffer bounds exportBounds 0 mempty


pushBuffer :: ReadableLogRecord -> BoundedBuffer -> Maybe BoundedBuffer
pushBuffer lr buf
  | bufCount buf >= bufBounds buf = Nothing
  | otherwise =
      Just $!
        buf
          { bufCount = bufCount buf + 1
          , bufItems = bufItems buf <> Builder.singleton lr
          }


buildExportBatch :: BoundedBuffer -> (BoundedBuffer, Vector ReadableLogRecord)
buildExportBatch buf =
  ( buf {bufCount = 0, bufItems = mempty}
  , Builder.build (bufItems buf)
  )


data ProcessorMessage = ScheduledFlush | MaxExportFlush | FlushRequested | Shutdown


batchLogRecordProcessor :: (MonadIO m) => BatchLogRecordProcessorConfig -> m LogRecordProcessor
batchLogRecordProcessor BatchLogRecordProcessorConfig {..} = liftIO $ do
  unless rtsSupportsBoundThreads $
    throwIO (userError "The threaded runtime is required for the batch log record processor")
  batch <- newIORef $ emptyBuffer batchLogMaxQueueSize batchLogMaxExportBatchSize
  droppedRef <- newIORef (0 :: Int)
  warnedRef <- newIORef False
  workSignal <- newEmptyTMVarIO
  shutdownSignal <- newEmptyTMVarIO
  flushRequestSignal <- newEmptyTMVarIO
  flushDoneSignal <- newEmptyTMVarIO

  let timeoutMicros = millisToMicros batchLogExportTimeoutMillis

  let publish batchToExport = do
        mResult <-
          timeout timeoutMicros $
            mask_ $
              logRecordExporterExport batchLogExporter batchToExport
        pure $ case mResult of
          Nothing -> Failure Nothing
          Just r -> r

  let publishBounded batchToExport
        | V.null batchToExport = pure Success
        | V.length batchToExport <= batchLogMaxExportBatchSize =
            publish batchToExport
        | otherwise = do
            let (chunk, rest) = V.splitAt batchLogMaxExportBatchSize batchToExport
            res <- publish chunk
            case res of
              Failure _ -> pure res
              Success -> publishBounded rest

  let flushQueueImmediately ret = do
        batchToExport <- atomicModifyIORef' batch buildExportBatch
        if V.null batchToExport
          then pure ret
          else do
            ret' <- publishBounded batchToExport
            flushQueueImmediately ret'

  -- Shutdown and FlushRequested are tried before work signals so they
  -- cannot be starved under sustained high throughput.
  let waiting = do
        delay <- registerDelay (millisToMicros batchLogScheduledDelayMillis)
        atomically $
          msum
            [ Shutdown <$ takeTMVar shutdownSignal
            , FlushRequested <$ takeTMVar flushRequestSignal
            , MaxExportFlush <$ takeTMVar workSignal
            , ScheduledFlush <$ do
                continue <- readTVar delay
                check continue
            ]

  let workerAction = do
        req <- waiting
        batchToExport <- atomicModifyIORef' batch buildExportBatch
        res <- publishBounded batchToExport
        case req of
          Shutdown -> flushQueueImmediately res
          FlushRequested -> do
            _ <- flushQueueImmediately res
            atomically $ putTMVar flushDoneSignal ()
            workerAction
          _ -> workerAction

  worker <- asyncWithUnmask $ \unmask -> unmask workerAction

  pure
    LogRecordProcessor
      { logRecordProcessorOnEmit = \lr _ctxt -> do
          readable <- mkReadableLogRecord lr
          (dropped, exportNeeded) <- atomicModifyIORef' batch $ \buf ->
            case pushBuffer readable buf of
              Nothing -> (buf, (True, True))
              Just b' ->
                if bufCount b' >= bufExportBounds b'
                  then (b', (False, True))
                  else (b', (False, False))
          when dropped $ warnOnDrop droppedRef warnedRef batchLogMaxQueueSize "BatchLogRecordProcessor"
          when exportNeeded $ void $ atomically $ tryPutTMVar workSignal ()
      , logRecordProcessorForceFlush = do
          atomically $ putTMVar flushRequestSignal ()
          atomically $ takeTMVar flushDoneSignal
          logRecordExporterForceFlush batchLogExporter
      , logRecordProcessorShutdown =
          mask $ \_restore -> do
            void $ atomically $ putTMVar shutdownSignal ()
            delay <- registerDelay (millisToMicros batchLogExportTimeoutMillis)
            shutdownResult <-
              atomically $
                msum
                  [ Just <$> waitCatchSTM worker
                  , Nothing <$ do
                      shouldStop <- readTVar delay
                      check shouldStop
                  ]
            cancel worker
            logRecordExporterShutdown batchLogExporter
            pure $ case shutdownResult of
              Nothing -> ShutdownTimeout
              Just (Left _) -> ShutdownFailure
              Just (Right _) -> ShutdownSuccess
      }
  where
    millisToMicros = (* 1000)


warnOnDrop :: IORef Int -> IORef Bool -> Int -> String -> IO ()
warnOnDrop droppedRef warnedRef capacity processorName = do
  n <- atomicModifyIORef' droppedRef (\c -> let c' = c + 1 in (c', c'))
  alreadyWarned <- atomicModifyIORef' warnedRef (\w -> (True, w))
  unless alreadyWarned $
    otelLogWarning $
      processorName
        <> ": queue full (capacity "
        <> show capacity
        <> "), dropping log record. Total dropped so far: "
        <> show n