packages feed

eventlog-live-otelcol-0.6.0.0: src/GHC/Eventlog/Live/Otelcol/Processor/Common/Profiles.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : GHC.Eventlog.Live.Otelcol.Processor.Common.Profiles
Description : Abstraction over ProfilesDictionary for the OTLP protocol.
Stability   : experimental
Portability : portable
-}
module GHC.Eventlog.Live.Otelcol.Processor.Common.Profiles (
  toExportProfileServiceRequest,

  -- * Dictionary for deduplication logic of common values
  ProfileDictionary,
  emptyProfileDictionary,

  -- * Turn a 'ProfileDictionary' into a 'OP.ProfilesDictionary'
  toProfilesDictionary,

  -- * Retrieve the 'SymbolIndex' for various 'OP.ProfilesData' fields
  SymbolIndex,
  getLocation,
  getFunction,
  getString,
  getMapping,
  getLink,
  getAttribute,
  getStack,
)
where

import Control.Monad.Trans.State.Strict (StateT)
import Control.Monad.Trans.State.Strict qualified as State
import Data.Int (Int32)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.ProtoLens (Message (..))
import Data.Text (Text)
import GHC.Eventlog.Live.Otelcol.Processor.Common.Core (messageWith)
import GHC.Generics (Generic)
import Lens.Family2 (Lens', (&), (.~), (^.))
import Lens.Family2.Unchecked (lens)
import Proto.Opentelemetry.Proto.Collector.Profiles.V1development.ProfilesService qualified as OPS
import Proto.Opentelemetry.Proto.Collector.Profiles.V1development.ProfilesService_Fields qualified as OPS
import Proto.Opentelemetry.Proto.Profiles.V1development.Profiles qualified as OP
import Proto.Opentelemetry.Proto.Profiles.V1development.Profiles_Fields qualified as OP

toExportProfileServiceRequest :: OP.ProfilesData -> OPS.ExportProfilesServiceRequest
toExportProfileServiceRequest profilesData =
  messageWith
    [ OPS.resourceProfiles .~ profilesData ^. OPS.resourceProfiles
    , OPS.dictionary .~ profilesData ^. OPS.dictionary
    ]

type SymbolIndex = Int32

data ProfileDictionary = ProfileDictionary
  { locationTable :: CommonSymbolTable OP.Location
  -- ^ Common 'OP.Location' table, first entry is the 'defMessage'.
  --   This holds for OTLP 1.9.0.
  , functionTable :: CommonSymbolTable OP.Function
  -- ^ Common 'OP.Function' table, first entry is the 'defMessage'.
  --   This holds for OTLP 1.9.0.
  , stringTable :: CommonSymbolTable Text
  -- ^ Common string table, the first entry must be "" per the protobuf
  --   documentation.
  --
  --   @
  --    // A common table for strings referenced by various messages.
  --    // string_table[0] must always be "".
  --    repeated string string_table = 5;
  --   @
  , mappingTable :: CommonSymbolTable OP.Mapping
  -- ^ Common 'OP.Mapping' table, first entry is the 'defMessage'.
  --   This holds for OTLP 1.9.0.
  , linkTable :: CommonSymbolTable OP.Link
  -- ^ Common 'OP.Link' table, first entry is the 'defMessage'.
  --   This holds for OTLP 1.9.0.
  , attributeTable :: CommonSymbolTable OP.KeyValueAndUnit
  -- ^ Common 'OP.KeyValueAndUnit' table, first entry is the 'defMessage'.
  --   This holds for OTLP 1.9.0.
  , stackTable :: CommonSymbolTable OP.Stack
  -- ^ Common 'OP.Stack' table, first entry is the 'defMessage'.
  --   This holds for OTLP 1.9.0.
  }
  deriving (Show, Ord, Eq, Generic)

toProfilesDictionary :: ProfileDictionary -> OP.ProfilesDictionary
toProfilesDictionary st =
  messageWith
    [ OP.locationTable .~ locationTableList st
    , OP.functionTable .~ functionTableList st
    , OP.stringTable .~ stringTableList st
    , OP.mappingTable .~ mappingTableList st
    , OP.linkTable .~ linkTableList st
    , OP.attributeTable .~ attributeTableList st
    , OP.stackTable .~ stackTableList st
    ]

emptyProfileDictionary :: ProfileDictionary
emptyProfileDictionary =
  ProfileDictionary
    { locationTable = commonSymbolTableFromList [defMessage]
    , functionTable = commonSymbolTableFromList [defMessage]
    , stringTable = commonSymbolTableFromList [""]
    , mappingTable = commonSymbolTableFromList [defMessage]
    , linkTable = commonSymbolTableFromList [defMessage]
    , attributeTable = commonSymbolTableFromList [defMessage]
    , stackTable = commonSymbolTableFromList [defMessage]
    }

locationTableList :: ProfileDictionary -> [OP.Location]
locationTableList st = reverse st.locationTable.contents

functionTableList :: ProfileDictionary -> [OP.Function]
functionTableList st = reverse st.functionTable.contents

stringTableList :: ProfileDictionary -> [Text]
stringTableList st = reverse st.stringTable.contents

mappingTableList :: ProfileDictionary -> [OP.Mapping]
mappingTableList st = reverse st.mappingTable.contents

linkTableList :: ProfileDictionary -> [OP.Link]
linkTableList st = reverse st.linkTable.contents

attributeTableList :: ProfileDictionary -> [OP.KeyValueAndUnit]
attributeTableList st = reverse st.attributeTable.contents

stackTableList :: ProfileDictionary -> [OP.Stack]
stackTableList st = reverse st.stackTable.contents

getSymbolIndexFor :: (Ord a, Monad m) => Lens' ProfileDictionary (CommonSymbolTable a) -> a -> StateT ProfileDictionary m SymbolIndex
getSymbolIndexFor accessor a = do
  tbl <- State.gets (^. accessor)
  let (idx, tbl1) = insertCommonSymbolTable a tbl
  State.modify' (\st -> st & accessor .~ tbl1)
  pure idx

getLocation :: (Monad m) => OP.Location -> StateT ProfileDictionary m SymbolIndex
getLocation = getSymbolIndexFor (lens (.locationTable) (\s v -> s{locationTable = v}))

getFunction :: (Monad m) => OP.Function -> StateT ProfileDictionary m SymbolIndex
getFunction = getSymbolIndexFor (lens (.functionTable) (\s v -> s{functionTable = v}))

getString :: (Monad m) => Text -> StateT ProfileDictionary m SymbolIndex
getString = getSymbolIndexFor (lens (.stringTable) (\s v -> s{stringTable = v}))

getMapping :: (Monad m) => OP.Mapping -> StateT ProfileDictionary m SymbolIndex
getMapping = getSymbolIndexFor (lens (.mappingTable) (\s v -> s{mappingTable = v}))

getLink :: (Monad m) => OP.Link -> StateT ProfileDictionary m SymbolIndex
getLink = getSymbolIndexFor (lens (.linkTable) (\s v -> s{linkTable = v}))

getAttribute :: (Monad m) => OP.KeyValueAndUnit -> StateT ProfileDictionary m SymbolIndex
getAttribute = getSymbolIndexFor (lens (.attributeTable) (\s v -> s{attributeTable = v}))

getStack :: (Monad m) => OP.Stack -> StateT ProfileDictionary m SymbolIndex
getStack = getSymbolIndexFor (lens (.stackTable) (\s v -> s{stackTable = v}))

-------------------------------------------------------------------------------
-- Common Symbol Table implementation
-- TODO: share with `ghc-stack-profiler-core` table?

data CommonSymbolTable a
  = CommonSymbolTable
  { counter :: !SymbolIndex
  , table :: Map a SymbolIndex
  , contents :: ![a]
  }
  deriving (Show, Ord, Eq, Generic)

emptyCommonSymbolTable :: CommonSymbolTable a
emptyCommonSymbolTable =
  CommonSymbolTable
    { counter = 0
    , table = Map.empty
    , contents = []
    }

commonSymbolTableFromList :: (Ord a) => [a] -> CommonSymbolTable a
commonSymbolTableFromList = foldr go emptyCommonSymbolTable
 where
  go val tbl0 =
    let (_, tbl1) = insertCommonSymbolTable val tbl0
     in tbl1

nextCounter :: CommonSymbolTable a -> (SymbolIndex, CommonSymbolTable a)
nextCounter tbl = (tbl.counter, tbl{counter = tbl.counter + 1})

insertCommonSymbolTable :: (Ord a) => a -> CommonSymbolTable a -> (SymbolIndex, CommonSymbolTable a)
insertCommonSymbolTable val tbl =
  let updateEntry tbl0 Nothing =
        let (sid, newTbl) = nextCounter tbl0
         in ((sid, True, newTbl), Just sid)
      updateEntry newTbl (Just old) =
        ((old, False, newTbl), Just old)

      ((idx, newEntry, tbl1), newTable) = Map.alterF (updateEntry tbl) val tbl.table
   in ( idx
      , tbl1
          { table = newTable
          , contents =
              if newEntry
                then val : tbl1.contents
                else tbl1.contents
          }
      )